├── .gitignore ├── .travis.yml ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ratpack-workflow-core ├── ratpack-workflow-core.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── danveloper │ │ └── ratpack │ │ └── workflow │ │ ├── FlowCompletionHandler.java │ │ ├── FlowConfigSource.java │ │ ├── FlowErrorHandler.java │ │ ├── FlowPreStartInterceptor.java │ │ ├── FlowStatus.java │ │ ├── FlowStatusRepository.java │ │ ├── MutableFlowStatus.java │ │ ├── MutableWorkStatus.java │ │ ├── Page.java │ │ ├── Work.java │ │ ├── WorkChain.java │ │ ├── WorkChainDecorator.java │ │ ├── WorkCompletionHandler.java │ │ ├── WorkConfigSource.java │ │ ├── WorkContext.java │ │ ├── WorkProcessor.java │ │ ├── WorkState.java │ │ ├── WorkStatus.java │ │ ├── WorkStatusRepository.java │ │ ├── handlers │ │ ├── FlowListHandler.java │ │ ├── FlowStatusGetHandler.java │ │ ├── FlowSubmissionHandler.java │ │ ├── WorkListHandler.java │ │ ├── WorkStatusGetHandler.java │ │ └── WorkSubmissionHandler.java │ │ ├── internal │ │ ├── DefaultFlowStatus.java │ │ ├── DefaultWorkChain.java │ │ ├── DefaultWorkContext.java │ │ ├── DefaultWorkProcessor.java │ │ ├── DefaultWorkStatus.java │ │ ├── DefaultWorkStatusDeserializer.java │ │ ├── ExceptionUtil.java │ │ ├── FlowProgressingWorkCompletionHandler.java │ │ ├── InMemoryFlowStatusRepository.java │ │ ├── InMemoryWorkStatusRepository.java │ │ ├── PrefixMatchingTypedVersionedWork.java │ │ ├── TypedVersionedWork.java │ │ ├── WorkChainWork.java │ │ └── WorkConfigSourceSerializer.java │ │ └── server │ │ ├── RatpackWorkflow.java │ │ ├── RatpackWorkflowServerSpec.java │ │ └── WorkChainConfig.java │ └── test │ └── groovy │ └── com │ └── danveloper │ └── ratpack │ └── workflow │ ├── FlowConfigSourceSpec.groovy │ ├── FunctionalSpec.groovy │ ├── RegistryFunctionalSpec.groovy │ ├── WorkflowServicesSpec.groovy │ ├── handlers │ ├── FlowListHandlerSpec.groovy │ ├── FlowStatusGetHandlerSpec.groovy │ ├── FlowSubmissionHandlerSpec.groovy │ ├── WorkListHandlerSpec.groovy │ └── WorkSubmissionHandlerSpec.groovy │ └── internal │ ├── DefaultWorkChainSpec.groovy │ ├── DefaultWorkContextSpec.groovy │ ├── DefaultWorkProcessorSpec.groovy │ ├── FlowStatusDeSerializerSpec.groovy │ ├── InMemoryFlowStatusRepositorySpec.groovy │ ├── TestObject.groovy │ └── WorkStatusDeSerializerSpec.groovy ├── ratpack-workflow-groovy ├── ratpack-workflow-groovy.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── danveloper │ │ └── ratpack │ │ └── workflow │ │ ├── GroovyWorkChain.java │ │ ├── groovy │ │ └── GroovyRatpackWorkflowMain.java │ │ ├── internal │ │ ├── DefaultGroovyWorkChain.java │ │ ├── StandaloneWorkflowScriptBacking.java │ │ └── capture │ │ │ ├── RatpackWorkflowDslBacking.java │ │ │ ├── RatpackWorkflowDslClosures.java │ │ │ └── RatpackWorkflowDslScriptCapture.java │ │ └── server │ │ ├── GroovyRatpackWorkflow.java │ │ └── GroovyRatpackWorkflowServerSpec.java │ └── test │ ├── groovy │ └── com │ │ └── danveloper │ │ └── ratpack │ │ └── workflow │ │ └── groovy │ │ ├── FunctionalGroovyDSLSpec.groovy │ │ ├── FunctionalGroovySpec.groovy │ │ ├── RatpackWorkflowGroovyScriptAppSpec.groovy │ │ ├── StubFlowRepo.groovy │ │ ├── StubWorkRepo.groovy │ │ └── internal │ │ ├── EmbeddedAppSpec.groovy │ │ └── GroovyRatpackWorkflowScriptAppSpec.groovy │ └── java │ └── com │ └── danveloper │ └── ratpack │ └── workflow │ └── groovy │ ├── GroovyRatpackWorkflowEmbeddedApp.java │ └── internal │ └── DefaultGroovyRatpackWorkflowEmbeddedApp.java ├── ratpack-workflow-redis ├── ratpack-workflow-redis.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── danveloper │ │ └── ratpack │ │ └── workflow │ │ └── redis │ │ ├── RedisFlowStatusRepository.java │ │ ├── RedisRepositorySupport.java │ │ └── RedisWorkStatusRepository.java │ └── test │ └── groovy │ └── com │ └── danveloper │ └── ratpack │ └── workflow │ └── redis │ ├── PortFinder.groovy │ ├── RedisFlowStatusRepositorySpec.groovy │ ├── RedisFunctionalSpec.groovy │ ├── RedisRepositorySpec.groovy │ ├── RedisWorkStatusRepositorySpec.groovy │ └── TestObject.groovy └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | out/ 3 | build/ 4 | .gradle/ 5 | .idea/ 6 | *.iml 7 | *.ipr 8 | *.iws 9 | .DS_Store 10 | *.log 11 | web/src/main/resources/app.yml 12 | *.rdb 13 | *.orig 14 | distributions/ 15 | web/src/main/resources/public/manual 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | install: true 2 | language: java 3 | script: 4 | - ./gradlew clean build 5 | cache: 6 | directories: 7 | - $HOME/.gradle 8 | jdk: 9 | - oraclejdk8 10 | env: 11 | global: 12 | - TERM=dumb 13 | sudo: false 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ratpack Workflow 2 | --- 3 | 4 | A workflow engine for Ratpack, built on the Ratpack Execution Model. Requires Ratpack 1.3.3. 5 | 6 | [![Build Status](https://travis-ci.org/danveloper/ratpack-workflow.svg)](https://travis-ci.org/danveloper/ratpack-workflow) 7 | 8 | Try it 9 | === 10 | 11 | Add this to your build.gradle: 12 | 13 | ```groovy 14 | repositories { 15 | maven { url 'http://oss.jfrog.org/artifactory/repo' } 16 | } 17 | 18 | dependencies { 19 | compile 'com.danveloper.ratpack.workflow:ratpack-workflow:0.3.3' 20 | } 21 | ``` 22 | 23 | Quick Start 24 | === 25 | 26 | The project ships with an extension to the regular Ratpack server specification. Nothing about your application needs to change except the class from which you import the `RatpackServer`. The below example shows importing the Groovy DSL from the `GroovyRatpackWorkflow` class, instead of Ratpack's `Groovy` class. 27 | 28 | ```groovy 29 | @Grab('com.danveloper.ratpack.workflow:ratpack-workflow-groovy:0.3.3') 30 | 31 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 32 | import static com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow.ratpack 33 | 34 | ratpack { 35 | workflow { 36 | work("type", "version") { 37 | // do one thing 38 | ... 39 | // trigger completion 40 | complete() 41 | } 42 | flow("name", "version") { 43 | all { 44 | // always run this 45 | ... 46 | // move on to the next 47 | next() 48 | } 49 | all { 50 | ... 51 | complete() 52 | } 53 | } 54 | } 55 | handlers { 56 | prefix("flows") { 57 | get(":id", RatpackWorkflow.flowStatusGetHandler()) 58 | get(RatpackWorkflow.flowListHandler()) 59 | post(RatpackWorkflow.flowSubmissionHandler()) 60 | } 61 | prefix("works") { 62 | get(":id", RatpackWorkflow.workStatusGetHandler()) 63 | get(RatpackWorkflow.workListHandler()) 64 | post(RatpackWorkflow.workSubmissionHandler()) 65 | } 66 | // ... 67 | } 68 | } 69 | ``` 70 | 71 | RESTful HTTP handlers are provided via static methods on the `RatpackWorkflow` class. You can use these endpoints to list jobs, lookup specific jobs, and submit jobs. 72 | 73 | Submitting Work 74 | === 75 | 76 | A properly formed JSON payload can be POSTed to the `FlowSubmissionHandler` to start a flow. 77 | 78 | ```javascript 79 | { 80 | "name": "AWESOME-FLOW-001" 81 | "description": "My awesome flow thingy", 82 | "tags": { 83 | "key": "val" 84 | }, 85 | "works": [ 86 | { 87 | "type": "workSomething", 88 | "version": "1.0", 89 | "data": { 90 | "key": "val", 91 | "key2": "val2" 92 | } 93 | }, 94 | { 95 | "type": "workSomethingElse", 96 | "version": "1.0", 97 | "data": { 98 | "somekey": "someval", 99 | "somekey2": "someval2" 100 | } 101 | } 102 | ] 103 | } 104 | ``` 105 | 106 | Given this JSON, a workflow definition like the following will be matched: 107 | 108 | ```groovy 109 | class WorkSomethingConfig { 110 | String key 111 | String key2 112 | } 113 | 114 | class WorkSomethingElseConfig { 115 | String somekey 116 | String somekey2 117 | } 118 | 119 | ratpack { 120 | workflow { 121 | work("workSomething", "1.0") { 122 | def workConfig = config.mapData(WorkSomethingConfig) 123 | assert workConfig.key == "val" 124 | assert workConfig.key2 == "val2" 125 | complete() 126 | } 127 | work("workSomethingElse", "1.0") { 128 | def workConfig = config.mapData(WorkSomethingElseConfig) 129 | assert workConfig.somekey == "someval" 130 | assert workConfig.somekey2 == "someval2" 131 | complete() 132 | } 133 | } 134 | handlers { 135 | // ... 136 | } 137 | } 138 | ``` 139 | 140 | Backing Persistence 141 | === 142 | 143 | By default, work and flow status will use the `InMemoryWorkStatusRepository` and `InMemoryFlowStatusRepository` repositories respectively. By including the `ratpack-workflow-redis` module in your project, you can elect to bind the `RedisWorkStatusRepository` and `RedisFlowStatusRepository` types via a registry definition. 144 | 145 | Redis support, by default, will connect to `localhost:6379`. Binding a custom `JedisPool` will allow you to configure connection properties. 146 | 147 | ```groovy 148 | import com.danveloper.ratpack.workflow.redis.* 149 | import static com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow.ratpack 150 | 151 | ratpack { 152 | bindings { 153 | bind(FlowStatusRepository, RedisFlowStatusRepository) 154 | bind(WorkStatusRepository, RedisWorkStatusRepository) 155 | } 156 | workflow { 157 | // ... 158 | } 159 | handlers { 160 | // ... 161 | } 162 | } 163 | ``` 164 | 165 | Work vs. Flows 166 | === 167 | 168 | There are two types of operations that can be submitted to the workflow processor: `work` and `flow`. A `work` type represents an atomic operation, whereas a `flow` type encapsulates many `work` types that can be chained together. 169 | 170 | Contributors 171 | --- 172 | 173 | * [Dan Woods](https://twitter.com/danveloper) 174 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:2.2.3' 7 | classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:1.0.2' 8 | } 9 | } 10 | 11 | apply plugin: 'com.github.kt3k.coveralls' 12 | apply plugin: 'artifactory' 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | } 18 | 19 | apply plugin: 'idea' 20 | apply plugin: 'groovy' 21 | apply plugin: 'jacoco' 22 | apply plugin: 'maven' 23 | apply plugin: 'maven-publish' 24 | apply plugin: 'artifactory' 25 | 26 | ext { 27 | ratpackVersion = "1.6.1" 28 | } 29 | 30 | jacoco { 31 | toolVersion = '0.7.1.201405082137' 32 | } 33 | 34 | group = "com.danveloper.ratpack.workflow" 35 | version = "0.3.6-SNAPSHOT" 36 | 37 | dependencies { 38 | testCompile "org.codehaus.groovy:groovy-all:2.4.3" 39 | testCompile "io.ratpack:ratpack-groovy-test:${ratpackVersion}" 40 | testCompile 'cglib:cglib-nodep:3.1' 41 | testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' 42 | } 43 | 44 | jacocoTestReport { 45 | reports { 46 | xml.enabled false 47 | csv.enabled false 48 | html.destination "${buildDir}/jacocoHtml" 49 | } 50 | } 51 | 52 | artifactory { 53 | contextUrl = 'http://oss.jfrog.org' 54 | } 55 | 56 | artifactoryPublish { task -> 57 | rootProject.artifactory { 58 | publish { 59 | repository { 60 | repoKey = version.endsWith("-SNAPSHOT") ? 'oss-snapshot-local' : 'oss-release-local' 61 | gradle.taskGraph.whenReady { taskGraph -> 62 | if (taskGraph.hasTask(task)) { 63 | username = bintrayUser 64 | password = bintrayKey 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | task sourceJar(type: Jar) { 73 | from sourceSets.main.allJava 74 | } 75 | 76 | publishing { 77 | publications { 78 | mavenJava(MavenPublication) { 79 | from components.java 80 | 81 | artifact sourceJar { 82 | classifier "sources" 83 | } 84 | } 85 | 86 | } 87 | } 88 | } 89 | 90 | coveralls { 91 | sourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs).files.absolutePath 92 | } 93 | 94 | task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { 95 | dependsOn = subprojects.test 96 | sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs) 97 | classDirectories = files(subprojects.sourceSets.main.output) 98 | executionData = files(subprojects.jacocoTestReport.executionData) 99 | reports { 100 | html.enabled = true 101 | xml.enabled = true 102 | csv.enabled = false 103 | xml.destination = "${buildDir}/reports/jacoco/test/jacocoTestReport.xml" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danveloper/ratpack-workflow/13de1e6433ed63c235f8ff14cf6414a945159ef8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 27 16:11:02 MDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env 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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >&- 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >&- 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ratpack-workflow-core/ratpack-workflow-core.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile "io.ratpack:ratpack-rx:${ratpackVersion}" 3 | compile "io.ratpack:ratpack-guice:${ratpackVersion}" 4 | //compile "org.javassist:javassist:jar:3.20.0-GA" 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/FlowCompletionHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.exec.Operation; 4 | import ratpack.registry.Registry; 5 | 6 | public interface FlowCompletionHandler { 7 | Operation complete(Registry registry, FlowStatus flowStatus); 8 | } 9 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/FlowConfigSource.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.io.ByteSource; 6 | import ratpack.config.ConfigData; 7 | import ratpack.util.Types; 8 | 9 | import java.util.LinkedHashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class FlowConfigSource { 14 | 15 | private final String name; 16 | private final String description; 17 | private final Map tags; 18 | private final List works; 19 | 20 | public FlowConfigSource(String name, String description, Map tags, List works) { 21 | this.name = name; 22 | this.description = description; 23 | this.tags = tags; 24 | this.works = works; 25 | } 26 | 27 | public static FlowConfigSource of(ConfigData configData) throws Exception { 28 | String name = configData.getRootNode().get("name").asText(); 29 | String description = configData.getRootNode().get("description").asText(); 30 | Map tags = Types.cast(configData.get("/tags", LinkedHashMap.class)); 31 | List works = Lists.newArrayList(); 32 | for (JsonNode node : configData.getRootNode().get("works")) { 33 | String json = node.toString(); 34 | try { 35 | ConfigData workConfig = ConfigData.of(d -> d.json(ByteSource.wrap(json.getBytes())).build()); 36 | WorkConfigSource workConfigSource = WorkConfigSource.of(workConfig); 37 | works.add(workConfigSource); 38 | } catch (Exception IGNORE) { 39 | throw new RuntimeException("error converting work config source"); 40 | } 41 | } 42 | return new FlowConfigSource(name, description, tags, works); 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public String getDescription() { 50 | return description; 51 | } 52 | 53 | public Map getTags() { 54 | return tags; 55 | } 56 | 57 | public List getWorks() { 58 | return works; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/FlowErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.registry.Registry; 4 | 5 | public interface FlowErrorHandler { 6 | void error(Registry registry, FlowStatus status); 7 | } 8 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/FlowPreStartInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.exec.Promise; 4 | 5 | public interface FlowPreStartInterceptor { 6 | 7 | Promise intercept(MutableFlowStatus flowStatus); 8 | } 9 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/FlowStatus.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import com.danveloper.ratpack.workflow.internal.DefaultFlowStatus; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="@class", defaultImpl = DefaultFlowStatus.class) 10 | public interface FlowStatus { 11 | String getId(); 12 | String getName(); 13 | String getDescription(); 14 | WorkState getState(); 15 | Long getStartTime(); 16 | Long getEndTime(); 17 | Map getTags(); 18 | List getWorks(); 19 | 20 | default MutableFlowStatus toMutable() { 21 | if (this instanceof MutableFlowStatus) { 22 | return (MutableFlowStatus)this; 23 | } else { 24 | throw new IllegalStateException("FlowStatus is not mutable"); 25 | } 26 | } 27 | 28 | default ImmutableFlowStatus toImmutable() { 29 | return new ImmutableFlowStatus(this); 30 | } 31 | 32 | static FlowStatus of(String id, FlowConfigSource config) { 33 | return DefaultFlowStatus.of(id, config); 34 | } 35 | 36 | static FlowStatus of(FlowConfigSource config) { 37 | return DefaultFlowStatus.of(config); 38 | } 39 | 40 | class ImmutableFlowStatus implements FlowStatus { 41 | private final FlowStatus delegate; 42 | 43 | private ImmutableFlowStatus(FlowStatus delegate) { 44 | this.delegate = delegate; 45 | } 46 | 47 | @Override 48 | public String getId() { 49 | return delegate.getId(); 50 | } 51 | 52 | @Override 53 | public String getName() { 54 | return delegate.getName(); 55 | } 56 | 57 | @Override 58 | public String getDescription() { 59 | return delegate.getDescription(); 60 | } 61 | 62 | @Override 63 | public WorkState getState() { 64 | return delegate.getState(); 65 | } 66 | 67 | @Override 68 | public Long getStartTime() { 69 | return delegate.getStartTime(); 70 | } 71 | 72 | @Override 73 | public Long getEndTime() { 74 | return delegate.getEndTime(); 75 | } 76 | 77 | @Override 78 | public Map getTags() { 79 | return delegate.getTags(); 80 | } 81 | 82 | @Override 83 | public List getWorks() { 84 | return delegate.getWorks(); 85 | } 86 | 87 | public MutableFlowStatus toMutable() { 88 | throw new IllegalStateException("FlowStatus is immutable."); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/FlowStatusRepository.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.exec.Promise; 4 | 5 | import java.util.List; 6 | 7 | public interface FlowStatusRepository { 8 | Promise create(FlowConfigSource config); 9 | Promise save(FlowStatus status); 10 | Promise get(String id); 11 | Promise> list(Integer offset, Integer limit); 12 | Promise> listRunning(Integer offset, Integer limit); 13 | Promise> findByTag(Integer offset, Integer limit, String key, String value); 14 | } 15 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/MutableFlowStatus.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import java.util.List; 4 | 5 | public interface MutableFlowStatus extends FlowStatus { 6 | void setStartTime(Long startTime); 7 | void setEndTime(Long endTime); 8 | void setState(WorkState state); 9 | void setWorks(List works); 10 | } 11 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/MutableWorkStatus.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | public interface MutableWorkStatus extends WorkStatus { 4 | 5 | void setState(WorkState state); 6 | void setStartTime(Long startTime); 7 | void setEndTime(Long endTime); 8 | void setError(String error); 9 | } 10 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/Page.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.util.List; 7 | 8 | public class Page { 9 | 10 | private final Integer offset; 11 | private final Integer limit; 12 | private final Integer numPages; 13 | private final List objs; 14 | 15 | @JsonCreator 16 | public Page(@JsonProperty("offset") Integer offset, @JsonProperty("limit") Integer limit, 17 | @JsonProperty("numPages") Integer numPages, @JsonProperty("objs") List objs) { 18 | this.offset = offset; 19 | this.limit = limit; 20 | this.numPages = numPages; 21 | this.objs = objs; 22 | } 23 | 24 | public Integer getOffset() { 25 | return offset; 26 | } 27 | 28 | public Integer getLimit() { 29 | return limit; 30 | } 31 | 32 | public Integer getNumPages() { 33 | return numPages; 34 | } 35 | 36 | public List getObjs() { 37 | return objs; 38 | } 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (this == o) return true; 43 | if (o == null || getClass() != o.getClass()) return false; 44 | 45 | Page page = (Page) o; 46 | 47 | if (offset != null ? !offset.equals(page.offset) : page.offset != null) return false; 48 | if (limit != null ? !limit.equals(page.limit) : page.limit != null) return false; 49 | if (numPages != null ? !numPages.equals(page.numPages) : page.numPages != null) return false; 50 | return !(objs != null ? !objs.equals(page.objs) : page.objs != null); 51 | 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | int result = offset != null ? offset.hashCode() : 0; 57 | result = 31 * result + (limit != null ? limit.hashCode() : 0); 58 | result = 31 * result + (numPages != null ? numPages.hashCode() : 0); 59 | result = 31 * result + (objs != null ? objs.hashCode() : 0); 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/Work.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | public interface Work { 4 | void handle(WorkContext ctx); 5 | } 6 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkChain.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import com.danveloper.ratpack.workflow.internal.DefaultWorkChain; 4 | import ratpack.func.Action; 5 | import ratpack.registry.Registry; 6 | 7 | import java.util.List; 8 | 9 | public interface WorkChain { 10 | default WorkChain all(Work work) { 11 | return work("", work::handle); 12 | } 13 | 14 | WorkChain all(Class work); 15 | 16 | default WorkChain work(String type, Work work) { 17 | return work(type, "", work); 18 | } 19 | 20 | WorkChain work(String type, String version, Work work); 21 | WorkChain work(String type, String version, Class work); 22 | 23 | default WorkChain flow(String type, Action subchain) throws Exception { 24 | return flow(type, "", subchain); 25 | } 26 | 27 | WorkChain flow(String type, String version, Action subchain) throws Exception; 28 | 29 | List getWorks(); 30 | 31 | default WorkChain insert(Action chain) throws Exception { 32 | chain.execute(this); 33 | return this; 34 | } 35 | 36 | static WorkChain of(Action configurer) throws Exception { 37 | return of(Registry.empty(), configurer); 38 | } 39 | 40 | static WorkChain of(Registry registry, Action configurer) throws Exception { 41 | DefaultWorkChain chain = new DefaultWorkChain(registry); 42 | configurer.execute(chain); 43 | return chain; 44 | } 45 | 46 | static WorkChain empty() throws Exception { 47 | return of(c -> {}); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkChainDecorator.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | public interface WorkChainDecorator { 4 | void decorate(WorkChain chain); 5 | } 6 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkCompletionHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.api.Nullable; 4 | import ratpack.exec.Operation; 5 | import ratpack.registry.Registry; 6 | 7 | public interface WorkCompletionHandler { 8 | /** 9 | * @deprecated use complete(Registry, WorkStatus) instead 10 | */ 11 | @Deprecated 12 | default Operation complete() { 13 | return complete(Registry.empty(), null); 14 | } 15 | 16 | Operation complete(Registry registry, @Nullable WorkStatus workStatus); 17 | } 18 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkConfigSource.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import com.danveloper.ratpack.workflow.internal.WorkConfigSourceSerializer; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import ratpack.config.ConfigData; 6 | 7 | @JsonSerialize(using = WorkConfigSourceSerializer.class) 8 | public class WorkConfigSource { 9 | 10 | private final ConfigData configData; 11 | private final TypeVersion typeVersion; 12 | 13 | public WorkConfigSource(ConfigData configData) { 14 | this.configData = configData; 15 | this.typeVersion = configData.get(TypeVersion.class); 16 | } 17 | 18 | public static WorkConfigSource of(ConfigData configData) { 19 | return new WorkConfigSource(configData); 20 | } 21 | 22 | public String getVersion() { 23 | return this.typeVersion.version; 24 | } 25 | 26 | public String getType() { 27 | return this.typeVersion.type; 28 | } 29 | 30 | public O get(String path, Class type) { 31 | return configData.get(path, type); 32 | } 33 | 34 | public O mapData(Class type) { 35 | return get("/data", type); 36 | } 37 | 38 | static class TypeVersion { 39 | String type; 40 | String version; 41 | 42 | public String getType() { 43 | return type; 44 | } 45 | 46 | public void setType(String type) { 47 | this.type = type; 48 | } 49 | 50 | public String getVersion() { 51 | return version; 52 | } 53 | 54 | public void setVersion(String version) { 55 | this.version = version; 56 | } 57 | 58 | @Override 59 | public boolean equals(Object o) { 60 | if (this == o) return true; 61 | if (o == null || getClass() != o.getClass()) return false; 62 | 63 | TypeVersion that = (TypeVersion) o; 64 | 65 | if (type != null ? !type.equals(that.type) : that.type != null) return false; 66 | return !(version != null ? !version.equals(that.version) : that.version != null); 67 | 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | int result = type != null ? type.hashCode() : 0; 73 | result = 31 * result + (version != null ? version.hashCode() : 0); 74 | return result; 75 | } 76 | } 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | 83 | WorkConfigSource that = (WorkConfigSource) o; 84 | 85 | if (configData.getRootNode() != null ? !configData.getRootNode().equals(that.configData.getRootNode()) : that.configData.getRootNode() != null) return false; 86 | return !(typeVersion != null ? !typeVersion.equals(that.typeVersion) : that.typeVersion != null); 87 | 88 | } 89 | 90 | @Override 91 | public int hashCode() { 92 | int result = configData.getRootNode() != null ? configData.getRootNode().hashCode() : 0; 93 | result = 31 * result + (typeVersion != null ? typeVersion.hashCode() : 0); 94 | return result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkContext.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.exec.Execution; 4 | import ratpack.registry.Registry; 5 | 6 | public interface WorkContext extends Registry { 7 | WorkStatus getStatus(); 8 | Long getStartTime(); 9 | Long getEndTime(); 10 | 11 | WorkConfigSource getConfig(); 12 | void message(String message); 13 | 14 | void insert(Registry registry, Work...works); 15 | void insert(Work...works); 16 | void next(); 17 | void next(Registry registry); 18 | void retry(); 19 | void retry(Registry registry); 20 | void fail(Throwable t); 21 | void complete(); 22 | Execution getExecution(); 23 | } 24 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkProcessor.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.exec.Promise; 4 | import ratpack.registry.Registry; 5 | import ratpack.service.Service; 6 | 7 | public interface WorkProcessor extends Service { 8 | Promise start(FlowStatus flowStatus); 9 | Promise start(WorkStatus workStatus); 10 | Promise start(WorkStatus workStatus, Registry registry); 11 | } 12 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkState.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | public enum WorkState { 4 | NOT_STARTED, RUNNING, SUSPENDED, COMPLETED, FAILED 5 | } 6 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkStatus.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import com.danveloper.ratpack.workflow.internal.DefaultWorkStatus; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | 6 | import java.util.List; 7 | 8 | @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="@class") 9 | public interface WorkStatus { 10 | String getId(); 11 | WorkConfigSource getConfig(); 12 | Long getStartTime(); 13 | Long getEndTime(); 14 | WorkState getState(); 15 | String getError(); 16 | List getMessages(); 17 | 18 | default MutableWorkStatus toMutable() { 19 | return (MutableWorkStatus)this; 20 | } 21 | 22 | static WorkStatus of(String id, WorkConfigSource config) { 23 | return DefaultWorkStatus.of(id, config); 24 | } 25 | 26 | static WorkStatus of(WorkConfigSource config) { 27 | return DefaultWorkStatus.of(config); 28 | } 29 | 30 | class WorkStatusMessage { 31 | private Long time; 32 | private String content; 33 | 34 | public WorkStatusMessage(Long time, String content) { 35 | this.time = time; 36 | this.content = content; 37 | } 38 | 39 | public void setTime(Long time) { 40 | this.time = time; 41 | } 42 | 43 | public void setContent(String content) { 44 | this.content = content; 45 | } 46 | 47 | public Long getTime() { 48 | return time; 49 | } 50 | 51 | public String getContent() { 52 | return content; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null || getClass() != o.getClass()) return false; 59 | 60 | WorkStatusMessage that = (WorkStatusMessage) o; 61 | 62 | if (time != null ? !time.equals(that.time) : that.time != null) return false; 63 | return !(content != null ? !content.equals(that.content) : that.content != null); 64 | 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | int result = time != null ? time.hashCode() : 0; 70 | result = 31 * result + (content != null ? content.hashCode() : 0); 71 | return result; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/WorkStatusRepository.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import ratpack.exec.Promise; 4 | 5 | public interface WorkStatusRepository { 6 | 7 | Promise create(WorkConfigSource source); 8 | 9 | Promise save(WorkStatus status); 10 | 11 | Promise> list(Integer offset, Integer limit); 12 | 13 | Promise> listRunning(Integer offset, Integer limit); 14 | 15 | Promise get(String id); 16 | 17 | Promise lock(String id); 18 | 19 | Promise unlock(String id); 20 | } 21 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/handlers/FlowListHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers; 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatus; 4 | import com.danveloper.ratpack.workflow.FlowStatusRepository; 5 | import com.danveloper.ratpack.workflow.Page; 6 | import ratpack.exec.Promise; 7 | import ratpack.handling.Context; 8 | import ratpack.handling.Handler; 9 | 10 | import java.util.Map; 11 | 12 | import static ratpack.jackson.Jackson.json; 13 | 14 | public class FlowListHandler implements Handler { 15 | @Override 16 | public void handle(Context ctx) throws Exception { 17 | Map qps = ctx.getRequest().getQueryParams(); 18 | Integer offset = qps.containsKey("offset") ? Integer.valueOf(qps.get("offset")) : 0; 19 | Integer limit = qps.containsKey("limit") ? Integer.valueOf(qps.get("limit")) : 10; 20 | Boolean running = qps.containsKey("running") ? Boolean.valueOf(qps.get("running")) : false; 21 | 22 | FlowStatusRepository flowStatusRepository = ctx.get(FlowStatusRepository.class); 23 | 24 | Promise> pagePromise = running ? flowStatusRepository.listRunning(offset, limit) : 25 | flowStatusRepository.list(offset, limit); 26 | pagePromise.then(page -> { 27 | ctx.render(json(page)); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/handlers/FlowStatusGetHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers; 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatusRepository; 4 | import ratpack.handling.Context; 5 | import ratpack.handling.Handler; 6 | 7 | import java.util.HashMap; 8 | 9 | import static ratpack.jackson.Jackson.json; 10 | 11 | public class FlowStatusGetHandler implements Handler { 12 | @Override 13 | public void handle(Context ctx) throws Exception { 14 | FlowStatusRepository flowStatusRepository = ctx.get(FlowStatusRepository.class); 15 | 16 | String id = ctx.getPathTokens().get("id"); 17 | if (id == null) { 18 | ctx.getResponse().status(400); 19 | ctx.render(json(new HashMap() {{ 20 | put("status", "400"); 21 | put("message", "bad request. An id is required."); 22 | }})); 23 | } else { 24 | flowStatusRepository.get(id).then(flowStatus -> ctx.render(json(flowStatus))); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/handlers/FlowSubmissionHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers; 2 | 3 | import com.danveloper.ratpack.workflow.FlowConfigSource; 4 | import com.danveloper.ratpack.workflow.FlowStatusRepository; 5 | import com.danveloper.ratpack.workflow.WorkProcessor; 6 | import com.google.common.collect.Maps; 7 | import com.google.common.io.ByteSource; 8 | import ratpack.config.ConfigData; 9 | import ratpack.handling.Context; 10 | import ratpack.handling.Handler; 11 | 12 | import java.util.Map; 13 | 14 | import static ratpack.jackson.Jackson.json; 15 | 16 | public class FlowSubmissionHandler implements Handler { 17 | @Override 18 | public void handle(Context ctx) throws Exception { 19 | FlowStatusRepository flowStatusRepository = ctx.get(FlowStatusRepository.class); 20 | WorkProcessor workProcessor = ctx.get(WorkProcessor.class); 21 | 22 | ctx.getRequest().getBody().flatMap(body -> { 23 | String json = body.getText(); 24 | ConfigData configData = ConfigData.of(d -> d 25 | .json(ByteSource.wrap(json.getBytes())) 26 | .build() 27 | ); 28 | return flowStatusRepository.create(FlowConfigSource.of(configData)); 29 | }).flatMap(flowStatus -> workProcessor.start(flowStatus)) 30 | .then(id -> { 31 | ctx.getResponse().status(202); 32 | Map resp = Maps.newHashMap(); 33 | resp.put("id", id); 34 | ctx.render(json(resp)); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/handlers/WorkListHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers; 2 | 3 | import com.danveloper.ratpack.workflow.Page; 4 | import com.danveloper.ratpack.workflow.WorkStatus; 5 | import com.danveloper.ratpack.workflow.WorkStatusRepository; 6 | import ratpack.exec.Promise; 7 | import ratpack.handling.Context; 8 | import ratpack.handling.Handler; 9 | 10 | import java.util.Map; 11 | 12 | import static ratpack.jackson.Jackson.json; 13 | 14 | public class WorkListHandler implements Handler { 15 | @Override 16 | public void handle(Context ctx) throws Exception { 17 | Map qps = ctx.getRequest().getQueryParams(); 18 | Integer offset = qps.containsKey("offset") ? Integer.valueOf(qps.get("offset")) : 0; 19 | Integer limit = qps.containsKey("limit") ? Integer.valueOf(qps.get("limit")) : 10; 20 | Boolean running = qps.containsKey("running") ? Boolean.valueOf(qps.get("running")) : false; 21 | 22 | WorkStatusRepository workStatusRepository = ctx.get(WorkStatusRepository.class); 23 | 24 | Promise> pagePromise = running ? workStatusRepository.listRunning(offset, limit) : 25 | workStatusRepository.list(offset, limit); 26 | pagePromise.then(page -> ctx.render(json(page))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/handlers/WorkStatusGetHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers; 2 | 3 | import com.danveloper.ratpack.workflow.WorkStatusRepository; 4 | import ratpack.handling.Context; 5 | import ratpack.handling.Handler; 6 | 7 | import java.util.HashMap; 8 | 9 | import static ratpack.jackson.Jackson.json; 10 | 11 | public class WorkStatusGetHandler implements Handler { 12 | @Override 13 | public void handle(Context ctx) throws Exception { 14 | WorkStatusRepository workStatusRepository = ctx.get(WorkStatusRepository.class); 15 | 16 | String id = ctx.getPathTokens().get("id"); 17 | if (id == null) { 18 | ctx.getResponse().status(400); 19 | ctx.render(json(new HashMap() {{ 20 | put("status", "400"); 21 | put("message", "bad request. An id is required."); 22 | }})); 23 | } else { 24 | workStatusRepository.get(id).then(workStatus -> ctx.render(json(workStatus))); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/handlers/WorkSubmissionHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers; 2 | 3 | import com.danveloper.ratpack.workflow.WorkConfigSource; 4 | import com.danveloper.ratpack.workflow.WorkProcessor; 5 | import com.danveloper.ratpack.workflow.WorkStatusRepository; 6 | import com.google.common.io.ByteSource; 7 | import ratpack.config.ConfigData; 8 | import ratpack.handling.Context; 9 | import ratpack.handling.Handler; 10 | 11 | import java.util.HashMap; 12 | 13 | import static ratpack.jackson.Jackson.json; 14 | 15 | public class WorkSubmissionHandler implements Handler { 16 | @Override 17 | public void handle(Context ctx) throws Exception { 18 | WorkProcessor workProcessor = ctx.get(WorkProcessor.class); 19 | WorkStatusRepository workStatusRepository = ctx.get(WorkStatusRepository.class); 20 | ctx.getRequest().getBody().flatMap(body -> { 21 | String json = body.getText(); 22 | ConfigData configData = ConfigData.of(d -> d 23 | .json(ByteSource.wrap(json.getBytes())) 24 | .build() 25 | ); 26 | return workStatusRepository.create(WorkConfigSource.of(configData)); 27 | }).flatMap(workProcessor::start) 28 | .then(id -> { 29 | ctx.getResponse().status(202); 30 | ctx.render(json(new HashMap() {{ 31 | put("id", id); 32 | }})); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultFlowStatus.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.FlowConfigSource; 4 | import com.danveloper.ratpack.workflow.MutableFlowStatus; 5 | import com.danveloper.ratpack.workflow.WorkState; 6 | import com.danveloper.ratpack.workflow.WorkStatus; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Random; 11 | import java.util.UUID; 12 | 13 | public class DefaultFlowStatus implements MutableFlowStatus { 14 | private String id; 15 | private String name; 16 | private String description; 17 | private Long startTime; 18 | private Long endTime; 19 | private WorkState state; 20 | private Map tags; 21 | private List works; 22 | 23 | public static DefaultFlowStatus of(FlowConfigSource config) { 24 | return of(new UUID(new Random().nextLong(), new Random().nextLong()).toString(), config); 25 | } 26 | 27 | public static DefaultFlowStatus of(String id, FlowConfigSource config) { 28 | DefaultFlowStatus status = new DefaultFlowStatus(); 29 | status.id = id; 30 | status.name = config.getName(); 31 | status.description = config.getDescription(); 32 | status.state = WorkState.NOT_STARTED; 33 | status.tags = config.getTags(); 34 | return status; 35 | } 36 | 37 | void setId(String id) { 38 | this.id = id; 39 | } 40 | 41 | void setName(String name) { 42 | this.name = name; 43 | } 44 | 45 | void setDescription(String description) { 46 | this.description = description; 47 | } 48 | 49 | public void setStartTime(Long time) { 50 | this.startTime = time; 51 | } 52 | 53 | public void setEndTime(Long time) { 54 | this.endTime = time; 55 | } 56 | 57 | public void setState(WorkState state) { 58 | this.state = state; 59 | } 60 | 61 | void setTags(Map tags) { 62 | this.tags = tags; 63 | } 64 | 65 | @Override 66 | public void setWorks(List works) { 67 | this.works = works; 68 | } 69 | 70 | @Override 71 | public String getId() { 72 | return this.id; 73 | } 74 | 75 | @Override 76 | public String getName() { 77 | return this.name; 78 | } 79 | 80 | @Override 81 | public String getDescription() { 82 | return this.description; 83 | } 84 | 85 | @Override 86 | public WorkState getState() { 87 | return this.state; 88 | } 89 | 90 | @Override 91 | public Long getStartTime() { 92 | return this.startTime; 93 | } 94 | 95 | @Override 96 | public Long getEndTime() { 97 | return this.endTime; 98 | } 99 | 100 | @Override 101 | public Map getTags() { 102 | return this.tags; 103 | } 104 | 105 | @Override 106 | public List getWorks() { 107 | return this.works; 108 | } 109 | 110 | @Override 111 | public boolean equals(Object o) { 112 | if (this == o) return true; 113 | if (o == null || getClass() != o.getClass()) return false; 114 | 115 | DefaultFlowStatus that = (DefaultFlowStatus) o; 116 | 117 | if (id != null ? !id.equals(that.id) : that.id != null) return false; 118 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 119 | if (description != null ? !description.equals(that.description) : that.description != null) return false; 120 | if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) return false; 121 | if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) return false; 122 | if (state != that.state) return false; 123 | if (tags != null ? !tags.equals(that.tags) : that.tags != null) return false; 124 | return !(works != null ? !works.equals(that.works) : that.works != null); 125 | 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | int result = id != null ? id.hashCode() : 0; 131 | result = 31 * result + (name != null ? name.hashCode() : 0); 132 | result = 31 * result + (description != null ? description.hashCode() : 0); 133 | result = 31 * result + (startTime != null ? startTime.hashCode() : 0); 134 | result = 31 * result + (endTime != null ? endTime.hashCode() : 0); 135 | result = 31 * result + (state != null ? state.hashCode() : 0); 136 | result = 31 * result + (tags != null ? tags.hashCode() : 0); 137 | result = 31 * result + (works != null ? works.hashCode() : 0); 138 | return result; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultWorkChain.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.Work; 4 | import com.danveloper.ratpack.workflow.WorkChain; 5 | import com.google.common.collect.Lists; 6 | import com.google.common.reflect.TypeToken; 7 | import ratpack.func.Action; 8 | import ratpack.registry.Registry; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.stream.Collectors; 13 | 14 | public class DefaultWorkChain implements WorkChain { 15 | private final List works = Lists.newArrayList(); 16 | private final Registry registry; 17 | 18 | public DefaultWorkChain(Registry registry) { 19 | this.registry = registry; 20 | } 21 | 22 | @Override 23 | public WorkChain all(Class work) { 24 | works.add(new TypedVersionedWork("", "", registry.get(work))); 25 | return this; 26 | } 27 | 28 | @Override 29 | public WorkChain work(String type, String version, Work work) { 30 | works.add(new TypedVersionedWork(type, version, work)); 31 | return this; 32 | } 33 | 34 | @Override 35 | public WorkChain work(String type, String version, Class work) { 36 | Optional workOption = registry.maybeGet(TypeToken.of(work)); 37 | if (!workOption.isPresent()) { 38 | throw new IllegalArgumentException("could not find class " + work + " in the registry!"); 39 | } 40 | return work(type, version, workOption.get()); 41 | } 42 | 43 | @Override 44 | public WorkChain flow(String type, String version, Action subchain) throws Exception { 45 | DefaultWorkChain sub = new DefaultWorkChain(registry); 46 | subchain.execute(sub); 47 | List subworks = sub.works.stream() 48 | .map(w -> { 49 | String wType = ((TypedVersionedWork)w).getType(); 50 | Work delegate = ((TypedVersionedWork)w).getDelegate(); 51 | String normalizedType = wType != null && wType.length() > 0 ? type + "/" + wType : wType; 52 | return new TypedVersionedWork(normalizedType, version, delegate); 53 | }) 54 | .collect(Collectors.toList()); 55 | 56 | WorkChainWork delegate = new WorkChainWork(subworks.toArray(new Work[subworks.size()])); 57 | Work work = new PrefixMatchingTypedVersionedWork(type, version, delegate); 58 | works.add(work); 59 | return this; 60 | } 61 | 62 | @Override 63 | public List getWorks() { 64 | return this.works; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultWorkContext.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.*; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.reflect.TypeToken; 6 | import io.netty.channel.EventLoop; 7 | import ratpack.exec.ExecController; 8 | import ratpack.exec.Execution; 9 | import ratpack.exec.Promise; 10 | import ratpack.registry.Registry; 11 | 12 | import java.util.Deque; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | import static com.danveloper.ratpack.workflow.internal.ExceptionUtil.exceptionToString; 18 | 19 | public class DefaultWorkContext implements WorkContext { 20 | private final WorkConstants workConstants; 21 | private final Registry registry; 22 | private static final Work end = ctx -> { 23 | throw new RuntimeException("work was never handled!"); 24 | }; 25 | 26 | @Override 27 | public WorkStatus getStatus() { 28 | return getContextRegistry().get(WorkStatus.class); 29 | } 30 | 31 | @Override 32 | public Long getStartTime() { 33 | return getStatus().getStartTime(); 34 | } 35 | 36 | @Override 37 | public Long getEndTime() { 38 | return getStatus().getEndTime(); 39 | } 40 | 41 | @Override 42 | public WorkConfigSource getConfig() { 43 | return registry.get(WorkConfigSource.class); 44 | } 45 | 46 | @Override 47 | public void message(String message) { 48 | DefaultWorkStatus status = (DefaultWorkStatus)getContextRegistry().get(WorkStatus.class); 49 | status.getMessages().add(new WorkStatus.WorkStatusMessage(System.currentTimeMillis(), message)); 50 | } 51 | 52 | @Override 53 | public void insert(Work... works) { 54 | if (works.length == 0) { 55 | throw new IllegalArgumentException("works is zero length"); 56 | } 57 | workConstants.indexes.push(new ChainIndex(works, registry, false)); 58 | next(); 59 | } 60 | 61 | @Override 62 | public void insert(final Registry registry, Work... works) { 63 | if (works.length == 0) { 64 | throw new IllegalArgumentException("works is zero length"); 65 | } 66 | workConstants.indexes.push(new ChainIndex(works, getContextRegistry().join(registry), false)); 67 | next(); 68 | } 69 | 70 | @Override 71 | public void next() { 72 | Work work = null; 73 | ChainIndex index = workConstants.indexes.peek(); 74 | 75 | if (index.i == 0) { 76 | WorkStatus status = registry.get(WorkStatus.class); 77 | if (status instanceof DefaultWorkStatus) { 78 | ((DefaultWorkStatus)status).setState(WorkState.RUNNING); 79 | ((DefaultWorkStatus)status).setStartTime(System.currentTimeMillis()); 80 | } else { 81 | throw new IllegalStateException("cannot update state"); 82 | } 83 | } 84 | 85 | while (work == null) { 86 | if (index.hasNext()) { 87 | work = index.next(); 88 | if (work instanceof WorkChainWork) { 89 | workConstants.indexes.push(new ChainIndex(((WorkChainWork) work).getWorks(), getContextRegistry(), false)); 90 | index = workConstants.indexes.peek(); 91 | work = null; 92 | } 93 | } else { 94 | workConstants.indexes.pop(); 95 | index = workConstants.indexes.peek(); 96 | } 97 | } 98 | 99 | try { 100 | work.handle(this); 101 | } catch (Exception e) { 102 | fail(e); 103 | } 104 | } 105 | 106 | @Override 107 | public void next(Registry registry) { 108 | workConstants.indexes.peek().registry = getContextRegistry().join(registry); 109 | next(); 110 | } 111 | 112 | @Override 113 | public void retry() { 114 | workConstants.indexes.peek().stepBack(); 115 | next(); 116 | } 117 | 118 | @Override 119 | public void retry(Registry registry) { 120 | workConstants.indexes.peek().stepBack(); 121 | workConstants.indexes.peek().registry = getContextRegistry().join(registry); 122 | next(); 123 | } 124 | 125 | @Override 126 | public void fail(Throwable t) { 127 | WorkStatus status = registry.get(WorkStatus.class); 128 | MutableWorkStatus mstatus = status.toMutable(); 129 | mstatus.setError(exceptionToString(t)); 130 | mstatus.setState(WorkState.FAILED); 131 | mstatus.setEndTime(System.currentTimeMillis()); 132 | } 133 | 134 | @Override 135 | public void complete() { 136 | this.workConstants.completed = true; 137 | } 138 | 139 | @Override 140 | public Execution getExecution() { 141 | return this.workConstants.execution; 142 | } 143 | 144 | @Override 145 | public Optional maybeGet(TypeToken type) { 146 | return getContextRegistry().maybeGet(type); 147 | } 148 | 149 | @Override 150 | public Iterable getAll(TypeToken type) { 151 | return getContextRegistry().getAll(type); 152 | } 153 | 154 | public static Promise start(Work[] works, WorkStatus workStatus, WorkStatusRepository workStatusRepository, Registry registry) throws Exception { 155 | EventLoop el = ExecController.require().getEventLoopGroup().next(); 156 | return start(works, el, workStatus, workStatusRepository, registry); 157 | } 158 | 159 | public static Promise start(Work[] works, WorkConfigSource workConfigSource, WorkStatusRepository workStatusRepository, Registry registry) throws Exception { 160 | EventLoop el = ExecController.require().getEventLoopGroup().next(); 161 | return start(works, el, workConfigSource, workStatusRepository, registry); 162 | } 163 | 164 | public static Promise start(Work[] works, EventLoop eventLoop, WorkConfigSource workConfigSource, WorkStatusRepository workStatusRepository, Registry registry) throws Exception { 165 | return workStatusRepository.create(workConfigSource) 166 | .flatMap(workStatus -> start(works, eventLoop, workStatus, workStatusRepository, registry)); 167 | } 168 | 169 | public static Promise start(Work[] works, EventLoop eventLoop, WorkStatus workStatus, WorkStatusRepository workStatusRepository, final Registry outerRegistry) throws Exception { 170 | Execution.fork() 171 | .eventLoop(eventLoop) 172 | .onError(t -> { 173 | WorkState resultState = WorkState.FAILED; 174 | MutableWorkStatus mstatus = workStatus.toMutable(); 175 | mstatus.setEndTime(System.currentTimeMillis()); 176 | mstatus.setState(resultState); 177 | mstatus.setError(ExceptionUtil.exceptionToString(t)); 178 | Execution.fork().start(e1 -> { 179 | workStatusRepository.save(workStatus).operation().then(); 180 | }); 181 | }) 182 | .start(e -> { 183 | WorkConstants workConstants = new WorkConstants(); 184 | workConstants.eventLoop = e.getEventLoop(); 185 | 186 | e.onComplete(() -> { 187 | WorkState resultState = workConstants.completed ? WorkState.COMPLETED : WorkState.FAILED; 188 | MutableWorkStatus mstatus = workStatus.toMutable(); 189 | mstatus.setEndTime(System.currentTimeMillis()); 190 | mstatus.setState(resultState); 191 | Execution.fork().start(e1 -> { 192 | e1.add(WorkStatus.class, workStatus); 193 | workStatusRepository.save(workStatus).operation().then(); 194 | List completionHandlers = Lists 195 | .newArrayList(workConstants.context.getAll(TypeToken.of(WorkCompletionHandler.class))); 196 | completionHandlers.forEach(handler -> handler.complete(workConstants.context, workStatus).then()); 197 | }); 198 | }); 199 | 200 | Registry subregistry = Registry.of(r -> r 201 | .add(WorkConstants.class, workConstants) 202 | .add(WorkConfigSource.class, workStatus.getConfig()) 203 | .add(WorkStatus.class, workStatus) 204 | ); 205 | Registry registry = outerRegistry.join(subregistry); 206 | ChainIndex endChainIndex = new ChainIndex(new Work[]{end}, registry, true); 207 | workConstants.indexes.push(endChainIndex); 208 | 209 | ChainIndex chainIndex = new ChainIndex(works, registry, true); 210 | workConstants.indexes.push(chainIndex); 211 | 212 | DefaultWorkContext context = new DefaultWorkContext(workConstants, registry); 213 | workConstants.context = context; 214 | 215 | workConstants.execution = e; 216 | e.add(WorkStatus.class, workStatus); 217 | context.next(); 218 | }); 219 | return Promise.value(workStatus.getId()); 220 | } 221 | 222 | DefaultWorkContext(WorkConstants workConstants, Registry registry) { 223 | this.workConstants = workConstants; 224 | this.registry = registry; 225 | } 226 | 227 | private Registry getContextRegistry() { 228 | return workConstants.indexes.peek().registry; 229 | } 230 | 231 | private static class WorkConstants { 232 | private Execution execution; 233 | private EventLoop eventLoop; 234 | private DefaultWorkContext context; 235 | private boolean completed; 236 | private final Deque indexes = Lists.newLinkedList(); 237 | } 238 | 239 | private static class ChainIndex implements Iterator { 240 | final Work[] works; 241 | Registry registry; 242 | final boolean first; 243 | int i; 244 | 245 | private ChainIndex(Work[] works, Registry registry, boolean first) { 246 | this.works = works; 247 | this.registry = registry; 248 | this.first = first; 249 | } 250 | 251 | public Work next() { 252 | return works[i++]; 253 | } 254 | 255 | public void stepBack() { 256 | i--; 257 | } 258 | 259 | @Override 260 | public boolean hasNext() { 261 | return i < works.length; 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultWorkProcessor.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.*; 4 | import com.danveloper.ratpack.workflow.server.WorkChainConfig; 5 | import com.google.common.collect.Iterables; 6 | import com.google.common.collect.Lists; 7 | import ratpack.exec.Promise; 8 | import ratpack.registry.Registry; 9 | import ratpack.service.StartEvent; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | public class DefaultWorkProcessor implements WorkProcessor { 15 | private Work[] works; 16 | private Registry registry; 17 | private WorkStatusRepository workStatusRepository; 18 | private FlowStatusRepository flowStatusRepository; 19 | private List flowPreStartInterceptors; 20 | 21 | @Override 22 | public void onStart(StartEvent event) throws Exception { 23 | registry = event.getRegistry(); 24 | 25 | boolean alreadyApplied = Iterables.any(registry.getAll(WorkCompletionHandler.class), 26 | h -> h.getClass().isAssignableFrom(FlowProgressingWorkCompletionHandler.class)); 27 | 28 | if (!alreadyApplied) { 29 | registry = registry.join(Registry.of(r -> r.add(WorkCompletionHandler.class, 30 | new FlowProgressingWorkCompletionHandler()))); 31 | } 32 | flowPreStartInterceptors = Lists.newArrayList(registry.getAll(FlowPreStartInterceptor.class)); 33 | flowPreStartInterceptors.add(new StatusFlowInterceptor()); 34 | WorkChainConfig config = registry.get(WorkChainConfig.class); 35 | workStatusRepository = registry.get(WorkStatusRepository.class); 36 | flowStatusRepository = registry.get(FlowStatusRepository.class); 37 | WorkChain chain = config.getWorkChainFunction().apply(registry); 38 | config.getAction().execute(chain); 39 | List workChainDecorators = Lists.newArrayList(registry.getAll(WorkChainDecorator.class)); 40 | workChainDecorators.forEach(decorator -> decorator.decorate(chain)); 41 | works = chain.getWorks().toArray(new Work[chain.getWorks().size()]); 42 | } 43 | 44 | @Override 45 | public Promise start(FlowStatus flowStatus) { 46 | if (flowStatus.getState() != WorkState.NOT_STARTED) { 47 | throw new IllegalStateException("Trying to start an already-started flow"); 48 | } 49 | if (flowStatus.getWorks().size() == 0) { 50 | throw new IllegalArgumentException("No work found for flow"); 51 | } 52 | 53 | MutableFlowStatus status = flowStatus.toMutable(); 54 | Promise p = null; 55 | for (FlowPreStartInterceptor interceptor : flowPreStartInterceptors) { 56 | if (p == null) { 57 | p = interceptor.intercept(status); 58 | } else { 59 | p = p.flatMap(s1 -> interceptor.intercept(s1.toMutable())); 60 | } 61 | } 62 | return p.flatMap(flowStatusRepository::save).flatMap(st -> 63 | start(st.getWorks().get(0), registry.join(Registry.single(FlowStatus.class, status)))).map(s -> status.getId()); 64 | } 65 | 66 | @Override 67 | public Promise start(WorkStatus workStatus) { 68 | return start(workStatus, registry); 69 | } 70 | 71 | @Override 72 | public Promise start(WorkStatus workStatus, Registry registry) { 73 | try { 74 | MutableWorkStatus mstatus = workStatus.toMutable(); 75 | mstatus.setStartTime(System.currentTimeMillis()); 76 | mstatus.setState(WorkState.RUNNING); 77 | return workStatusRepository.save(mstatus).flatMap(l -> 78 | DefaultWorkContext.start(works, workStatus, registry.get(WorkStatusRepository.class), registry) 79 | ); 80 | } catch (Exception e) { 81 | return Promise.async(f -> f.error(e)); 82 | } 83 | } 84 | 85 | private static class StatusFlowInterceptor implements FlowPreStartInterceptor { 86 | @Override 87 | public Promise intercept(MutableFlowStatus status) { 88 | status.setState(WorkState.RUNNING); 89 | status.setStartTime(System.currentTimeMillis()); 90 | return Promise.value(status); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultWorkStatus.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.MutableWorkStatus; 4 | import com.danveloper.ratpack.workflow.WorkConfigSource; 5 | import com.danveloper.ratpack.workflow.WorkState; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import com.google.common.collect.Lists; 8 | 9 | import java.util.List; 10 | import java.util.Random; 11 | import java.util.UUID; 12 | 13 | @JsonDeserialize(using = DefaultWorkStatusDeserializer.class) 14 | public class DefaultWorkStatus implements MutableWorkStatus { 15 | private String id; 16 | private WorkConfigSource config; 17 | private Long startTime; 18 | private Long endTime; 19 | private WorkState state; 20 | private String error; 21 | private List messages; 22 | 23 | DefaultWorkStatus() { 24 | this.messages = Lists.newArrayList(); 25 | } 26 | 27 | public static DefaultWorkStatus of(WorkConfigSource config) { 28 | String id = new UUID(new Random().nextLong(), new Random().nextLong()).toString(); 29 | return of(id, config); 30 | } 31 | 32 | public static DefaultWorkStatus of(String id, WorkConfigSource config) { 33 | DefaultWorkStatus status = new DefaultWorkStatus(); 34 | status.setId(id); 35 | status.setConfig(config); 36 | status.setState(WorkState.NOT_STARTED); 37 | status.setMessages(Lists.newArrayList()); 38 | 39 | return status; 40 | } 41 | 42 | void setId(String id) { 43 | this.id = id; 44 | } 45 | 46 | void setConfig(WorkConfigSource config) { 47 | this.config = config; 48 | } 49 | 50 | public void setStartTime(Long startTime) { 51 | this.startTime = startTime; 52 | } 53 | 54 | public void setEndTime(Long endTime) { 55 | this.endTime = endTime; 56 | } 57 | 58 | public void setState(WorkState state) { 59 | this.state = state; 60 | } 61 | 62 | public void setError(String error) { 63 | this.error = error; 64 | } 65 | 66 | void setMessages(List messages) { 67 | this.messages = messages; 68 | } 69 | 70 | @Override 71 | public String getId() { 72 | return this.id; 73 | } 74 | 75 | @Override 76 | public WorkConfigSource getConfig() { 77 | return this.config; 78 | } 79 | 80 | @Override 81 | public Long getStartTime() { 82 | return this.startTime; 83 | } 84 | 85 | @Override 86 | public Long getEndTime() { 87 | return this.endTime; 88 | } 89 | 90 | @Override 91 | public WorkState getState() { 92 | return this.state; 93 | } 94 | 95 | @Override 96 | public String getError() { 97 | return this.error; 98 | } 99 | 100 | @Override 101 | public List getMessages() { 102 | return this.messages; 103 | } 104 | 105 | @Override 106 | public boolean equals(Object o) { 107 | if (this == o) return true; 108 | if (o == null || getClass() != o.getClass()) return false; 109 | 110 | DefaultWorkStatus that = (DefaultWorkStatus) o; 111 | 112 | if (id != null ? !id.equals(that.id) : that.id != null) return false; 113 | if (config != null ? !config.equals(that.config) : that.config != null) return false; 114 | if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) return false; 115 | if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) return false; 116 | if (state != that.state) return false; 117 | if (error != null ? !error.equals(that.error) : that.error != null) return false; 118 | return !(messages != null ? !messages.equals(that.messages) : that.messages != null); 119 | 120 | } 121 | 122 | @Override 123 | public int hashCode() { 124 | int result = id != null ? id.hashCode() : 0; 125 | result = 31 * result + (config != null ? config.hashCode() : 0); 126 | result = 31 * result + (startTime != null ? startTime.hashCode() : 0); 127 | result = 31 * result + (endTime != null ? endTime.hashCode() : 0); 128 | result = 31 * result + (state != null ? state.hashCode() : 0); 129 | result = 31 * result + (error != null ? error.hashCode() : 0); 130 | result = 31 * result + (messages != null ? messages.hashCode() : 0); 131 | return result; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultWorkStatusDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.WorkConfigSource; 4 | import com.danveloper.ratpack.workflow.WorkState; 5 | import com.danveloper.ratpack.workflow.WorkStatus; 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.databind.DeserializationContext; 9 | import com.fasterxml.jackson.databind.JsonDeserializer; 10 | import com.fasterxml.jackson.databind.JsonNode; 11 | import com.google.common.collect.Lists; 12 | import com.google.common.io.ByteSource; 13 | import ratpack.config.ConfigData; 14 | 15 | import java.io.IOException; 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | public class DefaultWorkStatusDeserializer extends JsonDeserializer { 20 | @Override 21 | public DefaultWorkStatus deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 22 | JsonNode node = p.getCodec().readTree(p); 23 | String id = node.get("id").isNull() ? null : node.get("id").textValue(); 24 | Long startTime = node.get("startTime").isNull() ? null : node.get("startTime").asLong(); 25 | Long endTime = node.get("endTime").isNull() ? null : node.get("endTime").asLong(); 26 | WorkState state = node.get("state").isNull() ? null : WorkState.valueOf(node.get("state").asText()); 27 | List messages = Lists.newArrayList(node.get("messages").iterator()).stream().map(n -> { 28 | Long time = n.get("time").longValue(); 29 | String content = n.get("content").textValue(); 30 | return new WorkStatus.WorkStatusMessage(time, content); 31 | }).collect(Collectors.toList()); 32 | 33 | String config = node.get("config").isNull() ? null : node.get("config").deepCopy().toString(); 34 | ConfigData data; 35 | try { 36 | data = config == null ? null : ConfigData.of(d -> d.json(ByteSource.wrap(config.getBytes())).build()); 37 | } catch (Exception e) { 38 | throw new RuntimeException("Unable to deserialize WorkConfigData", e); 39 | } 40 | String error = node.get("error").isNull() ? null : node.get("error").toString(); 41 | DefaultWorkStatus status = new DefaultWorkStatus(); 42 | status.setId(id); 43 | status.setStartTime(startTime); 44 | status.setEndTime(endTime); 45 | status.setState(state); 46 | status.setError(error); 47 | status.setMessages(messages); 48 | status.setConfig(data != null ? WorkConfigSource.of(data) : null); 49 | 50 | return status; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/ExceptionUtil.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | 6 | public class ExceptionUtil { 7 | public static String exceptionToString(Throwable e) { 8 | StringWriter sw = new StringWriter(); 9 | e.printStackTrace(new PrintWriter(sw)); 10 | return sw.toString(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/FlowProgressingWorkCompletionHandler.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.*; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.collect.Sets; 6 | import ratpack.api.Nullable; 7 | import ratpack.exec.Operation; 8 | import ratpack.registry.Registry; 9 | import ratpack.service.Service; 10 | import ratpack.service.StartEvent; 11 | import ratpack.service.internal.DefaultEvent; 12 | import ratpack.stream.Streams; 13 | import ratpack.util.Exceptions; 14 | 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | public class FlowProgressingWorkCompletionHandler implements WorkCompletionHandler, Service { 19 | private boolean initialized; 20 | 21 | private Registry registry; 22 | private FlowStatusRepository flowStatusRepository; 23 | private List flowCompletionHandlers; 24 | private WorkProcessor workProcessor; 25 | 26 | @Override 27 | public void onStart(StartEvent event) throws Exception { 28 | registry = event.getRegistry(); 29 | flowCompletionHandlers = Lists.newArrayList(Sets.newLinkedHashSet(registry.getAll(FlowCompletionHandler.class))); 30 | workProcessor = registry.get(WorkProcessor.class); 31 | flowStatusRepository = registry.get(FlowStatusRepository.class); 32 | } 33 | 34 | @Override 35 | public Operation complete(Registry registry, @Nullable WorkStatus workStatus) { 36 | if (!initialized) { 37 | Exceptions.uncheck(() -> onStart(new DefaultEvent(registry, false))); 38 | } 39 | initialized = true; 40 | 41 | Optional flowStatusOption = registry.maybeGet(FlowStatus.class); 42 | Operation retVal = Operation.noop(); 43 | 44 | if (flowStatusOption.isPresent()) { 45 | FlowStatus f = flowStatusOption.get(); 46 | retVal = flowStatusRepository.get(f.getId()).next(flowStatus -> { 47 | if (workStatus.getState() == WorkState.COMPLETED) { 48 | Optional workOption = flowStatus.getWorks() 49 | .stream().filter(ws -> ws.getState() == WorkState.NOT_STARTED).findFirst(); 50 | if (workOption.isPresent()) { 51 | WorkStatus nextWorkStatus = workOption.get(); 52 | workProcessor.start(nextWorkStatus, registry).operation().then(); 53 | } else { 54 | completeFlow(flowStatus); 55 | } 56 | } else if (workStatus.getState() == WorkState.FAILED) { 57 | failFlow(flowStatus); 58 | } 59 | }).operation(); 60 | } 61 | 62 | return retVal; 63 | } 64 | 65 | private void failFlow(FlowStatus flow) { 66 | endFlow(flow, WorkState.FAILED); 67 | Optional o = registry.maybeGet(FlowErrorHandler.class); 68 | if (o.isPresent()) { 69 | FlowErrorHandler errorHandler = o.get(); 70 | errorHandler.error(registry, flow); 71 | } 72 | } 73 | 74 | private void completeFlow(FlowStatus flow) { 75 | endFlow(flow, WorkState.COMPLETED); 76 | } 77 | 78 | private void endFlow(FlowStatus flow, WorkState state) { 79 | MutableFlowStatus mflow = flow.toMutable(); 80 | mflow.setEndTime(System.currentTimeMillis()); 81 | mflow.setState(state); 82 | flowStatusRepository.save(mflow).operation().then(); 83 | Streams.publish(flowCompletionHandlers).flatMap(h -> h.complete(registry, flow.toImmutable()).promise()).toList() 84 | .operation().then(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/InMemoryFlowStatusRepository.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.*; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.collect.Maps; 6 | import com.google.common.collect.Sets; 7 | import ratpack.exec.Promise; 8 | import rx.Observable; 9 | 10 | import java.util.*; 11 | import java.util.stream.Collectors; 12 | 13 | import static ratpack.rx.RxRatpack.observe; 14 | import static ratpack.rx.RxRatpack.promiseSingle; 15 | 16 | public class InMemoryFlowStatusRepository implements FlowStatusRepository { 17 | private final Map storage = Maps.newConcurrentMap(); 18 | private final Map> tagStorage = Maps.newConcurrentMap(); 19 | 20 | private final WorkStatusRepository workStatusRepository; 21 | 22 | public InMemoryFlowStatusRepository(WorkStatusRepository workStatusRepository) { 23 | this.workStatusRepository = workStatusRepository; 24 | } 25 | 26 | @Override 27 | public Promise create(FlowConfigSource config) { 28 | Observable statusObs = Observable.just(new DefaultFlowStatus()).map(status -> { 29 | status.setId(new UUID(new Random().nextLong(), new Random().nextLong()).toString()); 30 | status.setName(config.getName()); 31 | status.setDescription(config.getDescription()); 32 | status.setTags(config.getTags()); 33 | status.setState(WorkState.NOT_STARTED); 34 | return status; 35 | }); 36 | 37 | Observable> workStatusesObs = Observable.from(config.getWorks()) 38 | .flatMap(workConfig -> observe(workStatusRepository.create(workConfig))).toList(); 39 | 40 | Observable zippedStatus = Observable.zip(statusObs, workStatusesObs, (status, workStatuses) -> { 41 | status.setWorks(workStatuses); 42 | storage.put(status.getId(), status); 43 | status.getTags().forEach((key, val) -> { 44 | String storageKey = "tags:"+key+":"+val; 45 | if (!tagStorage.containsKey(storageKey)) { 46 | tagStorage.put(storageKey, Sets.newConcurrentHashSet()); 47 | } 48 | tagStorage.get(storageKey).add(status.getId()); 49 | }); 50 | return status; 51 | }); 52 | 53 | return promiseSingle(zippedStatus).flatMap(st -> get(st.getId())); 54 | } 55 | 56 | @Override 57 | public Promise save(FlowStatus status) { 58 | if (status == null || status.getId() == null) { 59 | throw new IllegalArgumentException("status cannot be null"); 60 | } 61 | storage.put(status.getId(), status); 62 | return get(status.getId()); 63 | } 64 | 65 | @Override 66 | public Promise get(String id) { 67 | return Promise.value(storage.get(id)); 68 | } 69 | 70 | @Override 71 | public Promise> list(Integer offset, Integer limit) { 72 | Integer startIdx = limit * offset; 73 | Integer endIdx = limit + startIdx; 74 | List flows = Lists.newArrayList(storage.values()).subList(startIdx, endIdx > storage.size() ? storage.size() : endIdx); 75 | return Promise.value(new Page<>(offset, limit, (int)Math.ceil(storage.size() / limit), flows)); 76 | } 77 | 78 | @Override 79 | public Promise> listRunning(Integer offset, Integer limit) { 80 | Integer startIdx = limit * offset; 81 | Integer endIdx = limit + startIdx; 82 | List flows = storage.values().stream() 83 | .filter(st -> st.getState() == WorkState.RUNNING).collect(Collectors.toList()); 84 | List values = flows.subList(startIdx, endIdx > flows.size() ? flows.size() : endIdx); 85 | return Promise.value(new Page<>(offset, limit, (int)Math.ceil(flows.size() / limit), values)); 86 | } 87 | 88 | @Override 89 | public Promise> findByTag(Integer offset, Integer limit, String key, String value) { 90 | String storageKey = "tags:"+key+":"+value; 91 | if (!tagStorage.containsKey(storageKey)) { 92 | return Promise.value(new Page<>(offset, limit, 0, Lists.newArrayList())); 93 | } else { 94 | Integer startIdx = limit * offset; 95 | Integer endIdx = limit + startIdx; 96 | 97 | Set ids = tagStorage.get(storageKey); 98 | List taggedStatuses = ids.stream().map(storage::get).collect(Collectors.toList()); 99 | List values = taggedStatuses.subList(startIdx, endIdx > taggedStatuses.size() ? taggedStatuses.size() : endIdx); 100 | return Promise.value(new Page<>(offset, limit, (int)Math.ceil(taggedStatuses.size() / limit), values)); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/InMemoryWorkStatusRepository.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.*; 4 | import com.google.common.collect.Lists; 5 | import com.google.common.collect.Maps; 6 | import ratpack.exec.Promise; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Random; 11 | import java.util.UUID; 12 | import java.util.stream.Collectors; 13 | 14 | public class InMemoryWorkStatusRepository implements WorkStatusRepository { 15 | private final Map storage = Maps.newLinkedHashMap(); 16 | private final Map locks = Maps.newConcurrentMap(); 17 | 18 | @Override 19 | public Promise create(WorkConfigSource source) { 20 | String id = new UUID(new Random().nextLong(), new Random().nextLong()).toString(); 21 | 22 | DefaultWorkStatus status = new DefaultWorkStatus(); 23 | status.setId(id); 24 | status.setConfig(source); 25 | status.setState(WorkState.NOT_STARTED); 26 | status.setMessages(Lists.newArrayList()); 27 | 28 | storage.put(id, status); 29 | 30 | return get(id); 31 | } 32 | 33 | @Override 34 | public Promise save(WorkStatus status) { 35 | if (status == null || status.getId() == null) { 36 | throw new IllegalArgumentException("status or status id cannot be null"); 37 | } 38 | 39 | storage.put(status.getId(), status); 40 | return get(status.getId()); 41 | } 42 | 43 | @Override 44 | public Promise> list(Integer offset, Integer limit) { 45 | Integer startIdx = limit * offset; 46 | Integer endIdx = limit + startIdx; 47 | List values = Lists.newArrayList(storage.values()).subList(startIdx, endIdx > storage.size() ? storage.size() : endIdx); 48 | return Promise.value(new Page<>(offset, limit, (int)Math.ceil(storage.size() / limit), values)); 49 | } 50 | 51 | @Override 52 | public Promise> listRunning(Integer offset, Integer limit) { 53 | Integer startIdx = limit * offset; 54 | Integer endIdx = limit + startIdx; 55 | List works = storage.values().stream() 56 | .filter(st -> st.getState() == WorkState.RUNNING).collect(Collectors.toList()); 57 | List values = works.subList(startIdx, endIdx > works.size() ? works.size() : endIdx); 58 | return Promise.value(new Page<>(offset, limit, (int)Math.ceil(works.size() / limit), values)); 59 | } 60 | 61 | @Override 62 | public Promise get(String id) { 63 | if (id == null) { 64 | throw new IllegalArgumentException("id cannot be null"); 65 | } 66 | return Promise.value(storage.get(id)); 67 | } 68 | 69 | @Override 70 | public Promise lock(String id) { 71 | if (locks.containsKey(id)) { 72 | return Promise.value(Boolean.FALSE); 73 | } else { 74 | locks.put(id, Boolean.TRUE); 75 | return Promise.value(Boolean.TRUE); 76 | } 77 | } 78 | 79 | @Override 80 | public Promise unlock(String id) { 81 | if (locks.containsKey(id)) { 82 | locks.remove(id); 83 | } 84 | return Promise.value(Boolean.TRUE); 85 | } 86 | 87 | /** 88 | * only when you are sure of what you are doing 89 | * 90 | * @param id id of work to remove 91 | */ 92 | public void remove(String id) { 93 | storage.remove(id); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/PrefixMatchingTypedVersionedWork.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.Work; 4 | import com.danveloper.ratpack.workflow.WorkContext; 5 | 6 | public class PrefixMatchingTypedVersionedWork extends TypedVersionedWork { 7 | 8 | public PrefixMatchingTypedVersionedWork(String type, String version, Work delegate) { 9 | super(type, version, delegate); 10 | } 11 | 12 | public void handle(WorkContext ctx) { 13 | if (getType() != null && getVersion() != null && 14 | (getType().equals(ctx.getConfig().getType()) || ctx.getConfig().getType().startsWith(getType()+"/")) 15 | && getVersion().equals(ctx.getConfig().getVersion())) { 16 | getDelegate().handle(ctx); 17 | } else { 18 | ctx.next(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/TypedVersionedWork.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.Work; 4 | import com.danveloper.ratpack.workflow.WorkContext; 5 | 6 | public class TypedVersionedWork implements Work { 7 | 8 | private final String type; 9 | private final String version; 10 | private final Work delegate; 11 | 12 | TypedVersionedWork(String type, String version, Work delegate) { 13 | this.type = type; 14 | this.version = version; 15 | this.delegate = delegate; 16 | } 17 | 18 | @Override 19 | public void handle(WorkContext ctx) { 20 | if (("".equals(type) && "".equals(version)) || 21 | ("".equals(type) && version.equals(ctx.getConfig().getVersion())) || 22 | (type.equals(ctx.getConfig().getVersion()) && "".equals(version)) || 23 | (type.equals(ctx.getConfig().getType()) && version.equals(ctx.getConfig().getVersion()))) { 24 | delegate.handle(ctx); 25 | } else { 26 | ctx.next(); 27 | } 28 | } 29 | 30 | String getType() { 31 | return this.type; 32 | } 33 | 34 | String getVersion() { 35 | return this.version; 36 | } 37 | 38 | Work getDelegate() { 39 | return this.delegate; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/WorkChainWork.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.Work; 4 | import com.danveloper.ratpack.workflow.WorkContext; 5 | 6 | public class WorkChainWork implements Work { 7 | 8 | private final Work[] works; 9 | 10 | public WorkChainWork(Work[] works) { 11 | this.works = works; 12 | } 13 | 14 | @Override 15 | public void handle(WorkContext ctx) { 16 | ctx.insert(works); 17 | } 18 | 19 | public Work[] getWorks() { 20 | return this.works; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/internal/WorkConfigSourceSerializer.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.WorkConfigSource; 4 | import com.fasterxml.jackson.core.JsonGenerator; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.JsonSerializer; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | 9 | import java.io.IOException; 10 | import java.util.Map; 11 | 12 | public class WorkConfigSourceSerializer extends JsonSerializer { 13 | @Override 14 | public void serialize(WorkConfigSource value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { 15 | gen.getCodec().writeValue(gen, value.get("", Map.class)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/server/RatpackWorkflow.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.server; 2 | 3 | import com.danveloper.ratpack.workflow.handlers.*; 4 | import com.google.common.collect.Maps; 5 | import javassist.ClassPool; 6 | import javassist.CtClass; 7 | import javassist.CtMethod; 8 | import ratpack.func.Action; 9 | import ratpack.impose.Impositions; 10 | import ratpack.registry.Registry; 11 | import ratpack.server.RatpackServer; 12 | import ratpack.server.RatpackServerSpec; 13 | import ratpack.server.internal.DefaultRatpackServer; 14 | import ratpack.server.internal.ServerCapturer; 15 | import ratpack.util.Exceptions; 16 | 17 | import java.lang.reflect.InvocationHandler; 18 | import java.lang.reflect.Method; 19 | import java.lang.reflect.Proxy; 20 | import java.util.Map; 21 | 22 | public interface RatpackWorkflow { 23 | 24 | class RegistryHolder { 25 | public static Registry registry; 26 | } 27 | 28 | static RatpackServer of(Action definition) throws Exception { 29 | Action definitionAction = d -> { 30 | RatpackWorkflowServerSpec spec = new RatpackWorkflowServerSpec(d); 31 | definition.execute(spec); 32 | }; 33 | 34 | RatpackServer s1 = new DefaultRatpackServer(definitionAction, Impositions.current()); 35 | 36 | ServerCapturer.capture(s1); 37 | return s1; 38 | } 39 | 40 | static RatpackServer start(Action definition) throws Exception { 41 | RatpackServer server = of(definition); 42 | server.start(); 43 | return server; 44 | } 45 | 46 | static FlowListHandler flowListHandler() { 47 | return new FlowListHandler(); 48 | } 49 | 50 | static FlowStatusGetHandler flowStatusGetHandler() { 51 | return new FlowStatusGetHandler(); 52 | } 53 | 54 | static FlowSubmissionHandler flowSubmissionHandler() { 55 | return new FlowSubmissionHandler(); 56 | } 57 | 58 | static WorkListHandler workListHandler() { 59 | return new WorkListHandler(); 60 | } 61 | 62 | static WorkStatusGetHandler workStatusGetHandler() { 63 | return new WorkStatusGetHandler(); 64 | } 65 | 66 | static WorkSubmissionHandler workSubmissionHandler() { 67 | return new WorkSubmissionHandler(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/server/RatpackWorkflowServerSpec.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.server; 2 | 3 | import com.danveloper.ratpack.workflow.*; 4 | import com.danveloper.ratpack.workflow.internal.DefaultWorkProcessor; 5 | import com.danveloper.ratpack.workflow.internal.FlowProgressingWorkCompletionHandler; 6 | import com.danveloper.ratpack.workflow.internal.InMemoryFlowStatusRepository; 7 | import com.danveloper.ratpack.workflow.internal.InMemoryWorkStatusRepository; 8 | import ratpack.func.Action; 9 | import ratpack.func.Function; 10 | import ratpack.handling.Chain; 11 | import ratpack.handling.Handler; 12 | import ratpack.registry.Registry; 13 | import ratpack.registry.RegistrySpec; 14 | import ratpack.server.RatpackServerSpec; 15 | import ratpack.server.ServerConfig; 16 | import ratpack.server.ServerConfigBuilder; 17 | import ratpack.server.internal.DelegatingRatpackServerSpec; 18 | import ratpack.util.Exceptions; 19 | 20 | public class RatpackWorkflowServerSpec extends DelegatingRatpackServerSpec { 21 | private WorkChainConfig workChainConfig = new WorkChainConfig(); 22 | 23 | private Registry defaultRegistry = Exceptions.uncheck(() -> Registry.of(r -> { 24 | WorkStatusRepository workStatus = new InMemoryWorkStatusRepository(); 25 | r.add(WorkStatusRepository.class, workStatus) 26 | .add(WorkChainConfig.class, workChainConfig) 27 | .add(WorkProcessor.class, new DefaultWorkProcessor()) 28 | .add(WorkCompletionHandler.class, new FlowProgressingWorkCompletionHandler()) 29 | .add(FlowStatusRepository.class, new InMemoryFlowStatusRepository(workStatus)); 30 | } 31 | )); 32 | 33 | public RatpackWorkflowServerSpec(RatpackServerSpec delegate) { 34 | super(delegate); 35 | delegate.registry(defaultRegistry); 36 | } 37 | 38 | public RatpackWorkflowServerSpec workflow(Action action) throws Exception { 39 | workChainConfig.action = action; 40 | return this; 41 | } 42 | 43 | @Override 44 | public RatpackWorkflowServerSpec registry(Function function) { 45 | super.registry(r -> function.andThen(defaultRegistry::join).apply(r)); 46 | return this; 47 | } 48 | 49 | @Override 50 | public RatpackWorkflowServerSpec serverConfig(ServerConfig serverConfig) { 51 | super.serverConfig(serverConfig); 52 | return this; 53 | } 54 | 55 | @Override 56 | public RatpackWorkflowServerSpec handler(Function handlerFactory) { 57 | super.handler(handlerFactory); 58 | return this; 59 | } 60 | 61 | @Override 62 | public RatpackWorkflowServerSpec registryOf(Action action) throws Exception { 63 | super.registryOf(action); 64 | return this; 65 | } 66 | 67 | @Override 68 | public RatpackWorkflowServerSpec registry(Registry registry) { 69 | super.registry(registry); 70 | return this; 71 | } 72 | 73 | @Override 74 | public RatpackWorkflowServerSpec serverConfig(ServerConfigBuilder builder) { 75 | super.serverConfig(builder); 76 | return this; 77 | } 78 | 79 | @Override 80 | public RatpackWorkflowServerSpec serverConfig(Action action) throws Exception { 81 | super.serverConfig(action); 82 | return this; 83 | } 84 | 85 | @Override 86 | public RatpackWorkflowServerSpec handler(Class handlerType) { 87 | super.handler(handlerType); 88 | return this; 89 | } 90 | 91 | @Override 92 | public RatpackWorkflowServerSpec handlers(Action handlers) { 93 | super.handlers(handlers); 94 | return this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/main/java/com/danveloper/ratpack/workflow/server/WorkChainConfig.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.server; 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatusRepository; 4 | import com.danveloper.ratpack.workflow.WorkChain; 5 | import com.danveloper.ratpack.workflow.WorkStatusRepository; 6 | import com.danveloper.ratpack.workflow.internal.DefaultWorkChain; 7 | import com.danveloper.ratpack.workflow.internal.InMemoryFlowStatusRepository; 8 | import com.danveloper.ratpack.workflow.internal.InMemoryWorkStatusRepository; 9 | import ratpack.func.Action; 10 | import ratpack.func.Function; 11 | import ratpack.registry.Registry; 12 | 13 | public class WorkChainConfig { 14 | Action action = wc -> {}; 15 | Function workChainFunction = DefaultWorkChain::new; 16 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository(); 17 | Function flowStatusRepository = InMemoryFlowStatusRepository::new; 18 | 19 | public Action getAction() { 20 | return action; 21 | } 22 | 23 | public Function getWorkChainFunction() { 24 | return workChainFunction; 25 | } 26 | 27 | public WorkStatusRepository getWorkStatusRepository() { 28 | return workStatusRepository; 29 | } 30 | 31 | public Function getFlowStatusRepositoryFunction() { 32 | return flowStatusRepository; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/FlowConfigSourceSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow 2 | 3 | import com.google.common.io.ByteSource 4 | import ratpack.config.ConfigData 5 | import spock.lang.Specification 6 | 7 | class FlowConfigSourceSpec extends Specification { 8 | 9 | def json = """ 10 | { 11 | "name": "AWESOMEWORKFLOW-001-EL8_MODE", 12 | "description": "My really awesome workflow!", 13 | "tags": { 14 | "stack": "prod", 15 | "phase": "build" 16 | }, 17 | "works": [ 18 | { 19 | "type": "api/resize", 20 | "version": "1.0", 21 | "data": { 22 | "foo": "bar" 23 | } 24 | }, 25 | { 26 | "type": "api/scale", 27 | "version": "1.0", 28 | "data": { 29 | "direction": "up" 30 | } 31 | } 32 | ] 33 | } 34 | """ 35 | 36 | void "should map json into FlowConfigSource"() { 37 | setup: 38 | ConfigData data = ConfigData.of { d -> 39 | d.json(ByteSource.wrap(json.bytes)) 40 | .build() 41 | } 42 | 43 | when: 44 | def config = FlowConfigSource.of(data) 45 | 46 | then: 47 | config.name == "AWESOMEWORKFLOW-001-EL8_MODE" 48 | 2 == config.works.size() 49 | config.tags == [stack: "prod", phase: "build"] 50 | config.works.type == ["api/resize", "api/scale"] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/FunctionalSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow 2 | 3 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import ratpack.test.embed.EmbeddedApp 6 | import spock.lang.AutoCleanup 7 | import spock.lang.Specification 8 | 9 | import java.util.concurrent.CountDownLatch 10 | import java.util.concurrent.TimeUnit 11 | 12 | class FunctionalSpec extends Specification { 13 | 14 | static CountDownLatch latch 15 | 16 | def singleJson = """ 17 | { 18 | "type": "w", 19 | "version": "1.0", 20 | "data": { 21 | "foo": "bar" 22 | } 23 | } 24 | """ 25 | 26 | def flowJson = """ 27 | { 28 | "name": "AWESOMEWORKFLOW-001-EL8_MODE", 29 | "description": "My really awesome workflow!", 30 | "tags": { 31 | "stack": "prod", 32 | "phase": "build" 33 | }, 34 | "works": [ 35 | { 36 | "type": "w", 37 | "version": "1.0", 38 | "data": { 39 | "foo": "bar" 40 | } 41 | } 42 | ] 43 | } 44 | """ 45 | 46 | @AutoCleanup 47 | @Delegate 48 | EmbeddedApp app = EmbeddedApp.fromServer { 49 | RatpackWorkflow.of { spec -> spec 50 | .serverConfig { s -> s.port(0) } 51 | .workflow { chain -> chain 52 | .all { ctx -> 53 | latch.countDown() 54 | ctx.complete() 55 | } 56 | } 57 | .handlers { chain -> chain 58 | .prefix("ops") { pchain -> 59 | pchain.post(RatpackWorkflow.workSubmissionHandler()) 60 | .get(RatpackWorkflow.workListHandler()) 61 | .get(":id", RatpackWorkflow.workStatusGetHandler()) 62 | } 63 | .prefix("flows") { pchain -> 64 | pchain.post(RatpackWorkflow.flowSubmissionHandler()) 65 | .get(RatpackWorkflow.flowListHandler()) 66 | .get(":id", RatpackWorkflow.flowStatusGetHandler()) 67 | } 68 | } 69 | } 70 | } 71 | 72 | void "should be able to submit and retrieve work"() { 73 | setup: 74 | latch = new CountDownLatch(1) 75 | 76 | when: 77 | def resp = httpClient.requestSpec { spec -> spec 78 | .body { b -> 79 | b.bytes(singleJson.bytes) 80 | } 81 | .headers { h -> 82 | h.add("Content-Type", "application/json") 83 | } 84 | }.post("ops") 85 | 86 | and: 87 | def ref = new ObjectMapper().readValue(resp.body.text, Map) 88 | 89 | then: 90 | resp.statusCode == 202 91 | ref.containsKey("id") 92 | 93 | when: 94 | resp = httpClient.get("ops/${ref.id}".toString()) 95 | 96 | then: 97 | resp.statusCode == 200 98 | resp.headers.get("Content-Type") == "application/json" 99 | 100 | when: 101 | latch.await(1, TimeUnit.SECONDS) 102 | 103 | then: 104 | 0l == latch.count 105 | } 106 | 107 | void "should be able to submit and retrieve flows"() { 108 | setup: 109 | latch = new CountDownLatch(1) 110 | 111 | when: 112 | def resp = httpClient.requestSpec { spec -> spec 113 | .body { b -> 114 | b.bytes(flowJson.bytes) 115 | } 116 | .headers { h -> 117 | h.add("Content-Type", "application/json") 118 | } 119 | }.post("flows") 120 | 121 | and: 122 | def ref = new ObjectMapper().readValue(resp.body.text, Map) 123 | 124 | then: 125 | resp.statusCode == 202 126 | ref.containsKey("id") 127 | 128 | when: 129 | resp = httpClient.get("flows/${ref.id}".toString()) 130 | 131 | then: 132 | resp.statusCode == 200 133 | resp.headers.get("Content-Type") == "application/json" 134 | 135 | when: 136 | latch.await(1, TimeUnit.SECONDS) 137 | 138 | then: 139 | 0l == latch.count 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/RegistryFunctionalSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow 2 | 3 | import com.danveloper.ratpack.workflow.internal.TestObject 4 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import ratpack.guice.Guice 7 | import ratpack.test.embed.EmbeddedApp 8 | import spock.lang.AutoCleanup 9 | import spock.lang.Specification 10 | 11 | import java.util.concurrent.CountDownLatch 12 | import java.util.concurrent.TimeUnit 13 | 14 | class RegistryFunctionalSpec extends Specification { 15 | static CountDownLatch latch 16 | 17 | def singleJson = """ 18 | { 19 | "type": "w", 20 | "version": "1.0", 21 | "data": { 22 | "foo": "bar" 23 | } 24 | } 25 | """ 26 | 27 | @AutoCleanup 28 | @Delegate 29 | EmbeddedApp app = EmbeddedApp.fromServer { 30 | RatpackWorkflow.of { spec -> spec 31 | .serverConfig { s -> s.port(0) } 32 | .registry(Guice.registry { b -> b.bindInstance(new TestObject(foo: "bar"))}) 33 | .workflow { chain -> chain 34 | .all { ctx -> 35 | TestObject obj = ctx.get(TestObject) 36 | if (obj.foo != "bar") { 37 | throw new RuntimeException("!!") 38 | } 39 | ctx.complete() 40 | latch.countDown() 41 | } 42 | } 43 | .handlers { chain -> chain 44 | .prefix("ops") { pchain -> 45 | pchain.post(RatpackWorkflow.workSubmissionHandler()) 46 | .get(RatpackWorkflow.workListHandler()) 47 | .get(":id", RatpackWorkflow.workStatusGetHandler()) 48 | } 49 | } 50 | } 51 | } 52 | 53 | void "should be able to retrieve items from the registry within the work chain"() { 54 | setup: 55 | def mapper = new ObjectMapper() 56 | latch = new CountDownLatch(1) 57 | 58 | when: 59 | def resp = httpClient.requestSpec { spec -> spec 60 | .body { b -> 61 | b.bytes(singleJson.bytes) 62 | } 63 | .headers { h -> 64 | h.add("Content-Type", "application/json") 65 | } 66 | }.post("ops") 67 | 68 | and: 69 | latch.await(10, TimeUnit.SECONDS) 70 | 71 | and: 72 | def ref = mapper.readValue(resp.body.text, Map) 73 | 74 | and: 75 | resp = httpClient.get("ops/${ref.id}".toString()) 76 | 77 | and: 78 | def status = mapper.readValue(resp.body.text, Map) 79 | 80 | then: 81 | status.state == "COMPLETED" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/WorkflowServicesSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow 2 | 3 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 4 | import ratpack.service.Service 5 | import ratpack.service.StartEvent 6 | import ratpack.test.embed.EmbeddedApp 7 | import spock.lang.Issue 8 | import spock.lang.Specification 9 | 10 | import java.util.concurrent.atomic.LongAdder 11 | 12 | class WorkflowServicesSpec extends Specification { 13 | 14 | @Issue("#7 - Services are started twice during app start") 15 | def "services should only initialize once"() { 16 | setup: 17 | def addr = new LongAdder() 18 | def app = EmbeddedApp.fromServer { 19 | RatpackWorkflow.of { spec -> spec 20 | .workflow { chain -> 21 | chain.all { ctx -> 22 | ctx.complete() 23 | } 24 | } 25 | .registryOf { r -> 26 | r.add(Service, new Service() { 27 | @Override 28 | void onStart(StartEvent event) throws Exception { 29 | addr.increment() 30 | } 31 | }) 32 | } 33 | .handlers { chain -> 34 | chain.get { ctx -> ctx.render("Hello World")} 35 | } 36 | } 37 | } 38 | 39 | when: 40 | app.httpClient.get() 41 | 42 | then: 43 | addr.intValue() == 1 44 | 45 | cleanup: 46 | app.close() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/handlers/FlowListHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatusRepository 4 | import com.danveloper.ratpack.workflow.Page 5 | import com.danveloper.ratpack.workflow.WorkState 6 | import com.danveloper.ratpack.workflow.internal.DefaultFlowStatus 7 | import com.danveloper.ratpack.workflow.internal.DefaultWorkStatus 8 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 9 | import com.fasterxml.jackson.databind.ObjectMapper 10 | import ratpack.exec.Promise 11 | import ratpack.test.embed.EmbeddedApp 12 | import spock.lang.Specification 13 | 14 | class FlowListHandlerSpec extends Specification { 15 | 16 | FlowStatusRepository repo = Mock(FlowStatusRepository) 17 | ObjectMapper mapper = new ObjectMapper() 18 | 19 | @Delegate 20 | EmbeddedApp app = fromServer( 21 | RatpackWorkflow.of { spec -> 22 | spec 23 | .registryOf { r -> 24 | r.add(FlowStatusRepository, repo) 25 | } 26 | .serverConfig { d -> d.port(0) } 27 | .handlers { chain -> 28 | chain.all(new FlowListHandler()) 29 | } 30 | }) 31 | 32 | void "should list flows"() { 33 | setup: 34 | def flowStatus = new DefaultFlowStatus() 35 | flowStatus.id = 1 36 | flowStatus.name = "My Flow" 37 | flowStatus.description = "My Description" 38 | flowStatus.state = WorkState.FAILED 39 | flowStatus.startTime = System.currentTimeMillis() 40 | flowStatus.endTime = System.currentTimeMillis() 41 | flowStatus.works = [ 42 | new DefaultWorkStatus() 43 | ] 44 | def page = new Page<>(0, 10, 1, [flowStatus]) 45 | 46 | when: 47 | def resp = httpClient.get() 48 | 49 | then: 50 | 1 * repo.list(0, 10) >> { 51 | Promise.value(page) 52 | } 53 | mapper.writeValueAsString(page) == resp.body.text 54 | } 55 | 56 | void "should properly page results"() { 57 | setup: 58 | def flows = [] 59 | (1..30).each { int i -> 60 | def flowStatus = new DefaultFlowStatus() 61 | flowStatus.id = i 62 | flowStatus.name = "My Flow ${i}".toString() 63 | flowStatus.description = "My Description ${i}" 64 | flowStatus.state = i % 2 == 0 ? WorkState.FAILED : WorkState.COMPLETED 65 | flowStatus.startTime = System.currentTimeMillis() 66 | flowStatus.endTime = System.currentTimeMillis() 67 | flowStatus.works = [ 68 | new DefaultWorkStatus() 69 | ] 70 | flows << flowStatus 71 | } 72 | def page = new Page<>(0, 10, 3, flows) 73 | 74 | when: 75 | def resp = httpClient.get() 76 | 77 | then: 78 | 1 * repo.list(0, 10) >> { 79 | Promise.value(page) 80 | } 81 | mapper.writeValueAsString(page) == resp.body.text 82 | 83 | when: 84 | page = new Page<>(1, 10, 3, flows) 85 | 86 | and: 87 | resp = httpClient.get("?offset=1&limit=10") 88 | 89 | then: 90 | 1 * repo.list(1, 10) >> { 91 | Promise.value(page) 92 | } 93 | mapper.writeValueAsString(page) == resp.body.text 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/handlers/FlowStatusGetHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatusRepository 4 | import com.danveloper.ratpack.workflow.WorkState 5 | import com.danveloper.ratpack.workflow.internal.DefaultFlowStatus 6 | import com.danveloper.ratpack.workflow.internal.DefaultWorkStatus 7 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 8 | import com.fasterxml.jackson.databind.ObjectMapper 9 | import ratpack.exec.Promise 10 | import ratpack.func.Action 11 | import ratpack.test.embed.EmbeddedApp 12 | import spock.lang.AutoCleanup 13 | import spock.lang.Specification 14 | 15 | class FlowStatusGetHandlerSpec extends Specification { 16 | FlowStatusRepository repo = Mock(FlowStatusRepository) 17 | 18 | @AutoCleanup 19 | @Delegate 20 | EmbeddedApp app = EmbeddedApp.fromServer { 21 | RatpackWorkflow.of { spec -> 22 | spec 23 | .registryOf { r -> r.add(FlowStatusRepository, repo) } 24 | .serverConfig { d -> d.port(0) } 25 | .handlers { chain -> 26 | chain.get(":id", new FlowStatusGetHandler()) 27 | } 28 | } 29 | } 30 | 31 | void "should return a FlowStatus by id"() { 32 | setup: 33 | def flowStatus = new DefaultFlowStatus() 34 | flowStatus.id = 1 35 | flowStatus.name = "My Flow" 36 | flowStatus.description = "My Description" 37 | flowStatus.state = WorkState.FAILED 38 | flowStatus.startTime = System.currentTimeMillis() 39 | flowStatus.endTime = System.currentTimeMillis() 40 | flowStatus.works = [ 41 | new DefaultWorkStatus() 42 | ] 43 | 44 | when: 45 | def resp = httpClient.get("1") 46 | 47 | then: 48 | 1 * repo.get(_) >> { 49 | Promise.value(flowStatus) 50 | } 51 | new ObjectMapper().writeValueAsString(flowStatus) == resp.body.text 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/handlers/FlowSubmissionHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers 2 | 3 | import com.danveloper.ratpack.workflow.FlowConfigSource 4 | import com.danveloper.ratpack.workflow.FlowStatus 5 | import com.danveloper.ratpack.workflow.FlowStatusRepository 6 | import com.danveloper.ratpack.workflow.WorkProcessor 7 | import com.danveloper.ratpack.workflow.internal.DefaultFlowStatus 8 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 9 | import ratpack.exec.Promise 10 | import ratpack.test.embed.EmbeddedApp 11 | import spock.lang.AutoCleanup 12 | import spock.lang.Specification 13 | 14 | class FlowSubmissionHandlerSpec extends Specification { 15 | FlowStatusRepository repo = Mock(FlowStatusRepository) 16 | WorkProcessor workProcessor = Mock(WorkProcessor) 17 | 18 | def json = """ 19 | { 20 | "name": "AWESOMEWORKFLOW-001-EL8_MODE", 21 | "description": "My really awesome workflow!", 22 | "tags": { 23 | "stack": "prod", 24 | "phase": "build" 25 | }, 26 | "works": [ 27 | { 28 | "type": "api/resize", 29 | "version": "1.0", 30 | "data": { 31 | "foo": "bar" 32 | } 33 | }, 34 | { 35 | "type": "api/scale", 36 | "version": "1.0", 37 | "data": { 38 | "direction": "up" 39 | } 40 | } 41 | ] 42 | } 43 | """ 44 | 45 | @AutoCleanup 46 | @Delegate 47 | EmbeddedApp app = EmbeddedApp.fromServer { 48 | RatpackWorkflow.of { spec -> 49 | spec 50 | .registryOf { r -> 51 | r.add(WorkProcessor, workProcessor) 52 | r.add(FlowStatusRepository, repo) 53 | } 54 | .serverConfig { d -> d.port(0) } 55 | .handlers { chain -> 56 | chain.post(new FlowSubmissionHandler()) 57 | } 58 | } 59 | } 60 | 61 | void "should form a FlowConfigSource and submit for processing"() { 62 | setup: 63 | def flowStatus = Mock(DefaultFlowStatus) 64 | 65 | when: 66 | def resp = httpClient.requestSpec { spec -> 67 | spec.body { b -> 68 | b.text(json) 69 | } 70 | .headers { h -> 71 | h.set("Content-Type", "application/json") 72 | } 73 | }.post() 74 | 75 | then: 76 | resp.statusCode == 202 77 | 1 * repo.create(_) >> { FlowConfigSource config -> 78 | assert config.name == "AWESOMEWORKFLOW-001-EL8_MODE" 79 | assert config.description == "My really awesome workflow!" 80 | assert config.tags == [stack: "prod", phase: "build"] 81 | assert config.works.size() == 2 82 | return Promise.value(flowStatus) 83 | } 84 | 1 * workProcessor.start(_) >> { FlowStatus status -> 85 | assert status == flowStatus 86 | return Promise.value("1") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/handlers/WorkListHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers 2 | 3 | import com.danveloper.ratpack.workflow.Page 4 | import com.danveloper.ratpack.workflow.WorkStatusRepository 5 | import com.danveloper.ratpack.workflow.internal.DefaultWorkStatus 6 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 7 | import com.fasterxml.jackson.databind.ObjectMapper 8 | import ratpack.exec.Promise 9 | import ratpack.func.Action 10 | import ratpack.test.embed.EmbeddedApp 11 | import spock.lang.AutoCleanup 12 | import spock.lang.Specification 13 | 14 | class WorkListHandlerSpec extends Specification { 15 | WorkStatusRepository repo = Mock(WorkStatusRepository) 16 | 17 | @AutoCleanup 18 | @Delegate 19 | EmbeddedApp app = EmbeddedApp.fromServer { 20 | RatpackWorkflow.of { spec -> 21 | spec 22 | .registryOf { r -> r.add(WorkStatusRepository, repo) } 23 | .serverConfig { d -> d.port(0) } 24 | .handlers { chain -> 25 | chain.get(new WorkListHandler()) 26 | } 27 | } 28 | } 29 | 30 | void "should list flows"() { 31 | setup: 32 | def workStatus = new DefaultWorkStatus() 33 | workStatus.id = "1" 34 | def page = new Page<>(0, 10, 1, [workStatus]) 35 | 36 | when: 37 | def resp = httpClient.get() 38 | 39 | then: 40 | new ObjectMapper().writeValueAsString(page) == resp.body.text 41 | 1 * repo.list(0, 10) >> { 42 | Promise.value(page) 43 | } 44 | } 45 | 46 | void "should properly page results"() { 47 | setup: 48 | def works = [] 49 | (1..30).each { i -> 50 | def workStatus = new DefaultWorkStatus() 51 | workStatus.id = "${i}".toString() 52 | works << workStatus 53 | } 54 | def page = new Page<>(0, 10, 3, [works]) 55 | 56 | when: 57 | def resp = httpClient.get() 58 | 59 | then: 60 | new ObjectMapper().writeValueAsString(page) == resp.body.text 61 | 1 * repo.list(0, 10) >> { 62 | Promise.value(page) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/handlers/WorkSubmissionHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.handlers 2 | 3 | import com.danveloper.ratpack.workflow.WorkConfigSource 4 | import com.danveloper.ratpack.workflow.WorkProcessor 5 | import com.danveloper.ratpack.workflow.WorkStatus 6 | import com.danveloper.ratpack.workflow.WorkStatusRepository 7 | import com.danveloper.ratpack.workflow.internal.DefaultWorkStatus 8 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow 9 | import ratpack.exec.Promise 10 | import ratpack.registry.Registry 11 | import ratpack.test.embed.EmbeddedApp 12 | import spock.lang.AutoCleanup 13 | import spock.lang.Specification 14 | 15 | class WorkSubmissionHandlerSpec extends Specification { 16 | WorkStatusRepository repo = Mock(WorkStatusRepository) 17 | WorkProcessor workProcessor = Mock(WorkProcessor) 18 | 19 | def json = """ 20 | { 21 | "type": "api/resize", 22 | "version": "1.0", 23 | "data": { 24 | "foo": "bar" 25 | } 26 | } 27 | """ 28 | 29 | @AutoCleanup 30 | @Delegate 31 | EmbeddedApp app = EmbeddedApp.fromServer { 32 | RatpackWorkflow.of { spec -> 33 | spec 34 | .registryOf { r -> 35 | r.add(WorkProcessor, workProcessor) 36 | r.add(WorkStatusRepository, repo) 37 | } 38 | .serverConfig { d -> d.port(0) } 39 | .handlers { chain -> 40 | chain.post(new WorkSubmissionHandler()) 41 | } 42 | } 43 | } 44 | 45 | void "should form a WorkConfigSource and submit for processing"() { 46 | setup: 47 | def workStatus = Mock(DefaultWorkStatus) 48 | workStatus.id = "1" 49 | 50 | when: 51 | def resp = httpClient.requestSpec { spec -> 52 | spec.body { b -> 53 | b.text(json) 54 | } 55 | .headers { h -> 56 | h.set("Content-Type", "application/json") 57 | } 58 | }.post() 59 | 60 | then: 61 | resp.statusCode == 202 62 | 1 * repo.create(_) >> { WorkConfigSource config -> 63 | return Promise.value(workStatus) 64 | } 65 | 1 * workProcessor.start(_) >> { WorkStatus status -> 66 | assert status == workStatus 67 | return Promise.value("1") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/internal/DefaultWorkChainSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal 2 | 3 | import com.danveloper.ratpack.workflow.WorkChain 4 | import ratpack.func.Action 5 | import ratpack.registry.Registry 6 | import spock.lang.Specification 7 | 8 | class DefaultWorkChainSpec extends Specification { 9 | 10 | void "should compose a chain"() { 11 | given: 12 | DefaultWorkChain chain = new DefaultWorkChain(Registry.empty()) 13 | .work("resize", "1.0") {} 14 | .work("autoscale") {} 15 | .all {} 16 | .flow("flow") { schain -> 17 | schain.work("foo") {} 18 | } 19 | TypedVersionedWork flowwork = chain.works.find { TypedVersionedWork w -> w.getType() == "flow" } 20 | 21 | expect: 22 | 4 == chain.works.size() 23 | ["resize", "autoscale", "", "flow"] == chain.works.collect { TypedVersionedWork w -> w.getType() } 24 | flowwork.getDelegate() instanceof WorkChainWork 25 | 1 == flowwork.getDelegate().works.size() 26 | flowwork.getDelegate().works[0].getType() == "flow/foo" 27 | } 28 | 29 | void "should compose a chain from a subchain action"() { 30 | given: 31 | def act = { WorkChain chain -> 32 | chain.all { } 33 | } as Action 34 | DefaultWorkChain chain = new DefaultWorkChain(Registry.empty()).insert(act) 35 | 36 | expect: 37 | 1 == chain.works.size() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/internal/DefaultWorkProcessorSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal 2 | 3 | import com.danveloper.ratpack.workflow.* 4 | import com.danveloper.ratpack.workflow.server.WorkChainConfig 5 | import com.google.common.io.ByteSource 6 | import ratpack.config.ConfigData 7 | import ratpack.exec.Promise 8 | import ratpack.func.Action 9 | import ratpack.registry.Registry 10 | import ratpack.service.internal.DefaultEvent 11 | import ratpack.test.exec.ExecHarness 12 | import spock.lang.AutoCleanup 13 | import spock.lang.Specification 14 | 15 | import java.util.concurrent.CountDownLatch 16 | import java.util.concurrent.TimeUnit 17 | import java.util.concurrent.atomic.AtomicInteger 18 | 19 | class DefaultWorkProcessorSpec extends Specification { 20 | 21 | @AutoCleanup 22 | ExecHarness execHarness = ExecHarness.harness(Runtime.getRuntime().availableProcessors()) 23 | 24 | def json = """ 25 | { 26 | "name": "AWESOMEWORKFLOW-001-EL8_MODE", 27 | "description": "My really awesome workflow!", 28 | "tags": { 29 | "stack": "prod", 30 | "phase": "build" 31 | }, 32 | "works": [ 33 | { 34 | "type": "foo", 35 | "version": "1.0", 36 | "data": { 37 | "foo": "bar" 38 | } 39 | }, 40 | { 41 | "type": "bar", 42 | "version": "1.0", 43 | "data": { 44 | "direction": "up" 45 | } 46 | } 47 | ] 48 | } 49 | """ 50 | def configData = ConfigData.of { d -> 51 | d.json(ByteSource.wrap(json.bytes)).build() 52 | } 53 | 54 | void "should invoke all work for a given description and chain"() { 55 | setup: 56 | CountDownLatch latch = new CountDownLatch(3) 57 | Action actChain = { chain -> chain 58 | .work("foo", "1.0") { ctx -> 59 | latch.countDown() 60 | ctx.next() 61 | } 62 | .work("bar", "1.0") { ctx -> 63 | latch.countDown() 64 | ctx.next() 65 | } 66 | .all { ctx -> 67 | latch.countDown() 68 | ctx.complete() 69 | } 70 | } 71 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 72 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 73 | def workChainConfig = new WorkChainConfig() 74 | workChainConfig.action = actChain 75 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 76 | Registry registry = Registry.of() { r -> 77 | r.add(WorkStatusRepository, workStatusRepository) 78 | r.add(FlowStatusRepository, flowStatusRepository) 79 | r.add(WorkChainConfig, workChainConfig) 80 | r.add(WorkProcessor, processor) 81 | } 82 | 83 | when: 84 | execHarness.run { 85 | processor.onStart(new DefaultEvent(registry, false)) 86 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 87 | processor.start(flowStatus).operation().then() 88 | } 89 | } 90 | 91 | and: 92 | latch.await(10, TimeUnit.SECONDS) 93 | 94 | then: 95 | 0l == latch.count 96 | } 97 | 98 | void "should fail a flow if one of the works has failed"() { 99 | setup: 100 | AtomicInteger adder = new AtomicInteger() 101 | Action actChain = { chain -> chain 102 | .work("foo", "1.0") { ctx -> 103 | throw new RuntimeException("!!") 104 | } 105 | .work("bar", "1.0") { ctx -> 106 | adder.incrementAndGet() 107 | ctx.next() 108 | } 109 | .all { ctx -> 110 | adder.incrementAndGet() 111 | } 112 | } 113 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 114 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 115 | def workChainConfig = new WorkChainConfig() 116 | workChainConfig.action = actChain 117 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 118 | Registry registry = Registry.of() { r -> 119 | r.add(WorkStatusRepository, workStatusRepository) 120 | r.add(FlowStatusRepository, flowStatusRepository) 121 | r.add(WorkChainConfig, workChainConfig) 122 | r.add(WorkProcessor, processor) 123 | } 124 | 125 | when: 126 | execHarness.run { 127 | processor.onStart(new DefaultEvent(registry, false)) 128 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 129 | processor.start(flowStatus).operation().then() 130 | } 131 | } 132 | 133 | and: 134 | execHarness.controller.eventLoopGroup.awaitTermination(5, TimeUnit.SECONDS) 135 | 136 | and: 137 | def page = execHarness.yield { 138 | flowStatusRepository.list(0, 10) 139 | }.valueOrThrow 140 | 141 | and: 142 | def flows = page.objs 143 | 144 | then: 145 | 0 == adder.get() 146 | 1 == flows.size() 147 | flows[0].state == WorkState.FAILED 148 | flows[0].works[0].state == WorkState.FAILED 149 | flows[0].works[0].error.startsWith("java.lang.RuntimeException: !!") 150 | flows[0].works[1].state == WorkState.NOT_STARTED 151 | } 152 | 153 | void "should invoke interceptors before flows start"() { 154 | setup: 155 | def latch = new CountDownLatch(1) 156 | def invocationList = [] 157 | FlowPreStartInterceptor i = { s -> latch.countDown(); invocationList << 1; Promise.value(s) } 158 | Action actChain = { chain -> chain.all { c -> invocationList << 2; c.complete() } } 159 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 160 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 161 | def workChainConfig = new WorkChainConfig() 162 | workChainConfig.action = actChain 163 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 164 | Registry registry = Registry.of() { r -> 165 | r.add(WorkStatusRepository, workStatusRepository) 166 | r.add(FlowStatusRepository, flowStatusRepository) 167 | r.add(WorkChainConfig, workChainConfig) 168 | r.add(FlowPreStartInterceptor, i) 169 | r.add(WorkProcessor, processor) 170 | } 171 | 172 | when: 173 | execHarness.run { 174 | processor.onStart(new DefaultEvent(registry, false)) 175 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 176 | processor.start(flowStatus).operation().then() 177 | } 178 | } 179 | 180 | and: 181 | execHarness.controller.eventLoopGroup.awaitTermination(5, TimeUnit.SECONDS) 182 | 183 | and: 184 | latch.await(5, TimeUnit.SECONDS) 185 | 186 | then: 187 | [1, 2, 2] == invocationList 188 | 0l == latch.count 189 | } 190 | 191 | void "should invoke error handler when error occurs"() { 192 | setup: 193 | CountDownLatch latch = new CountDownLatch(1) 194 | Action actChain = { chain -> chain 195 | .all { 196 | throw new RuntimeException() 197 | } 198 | } 199 | FlowErrorHandler errorHandler = { r, f -> latch.countDown() } 200 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 201 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 202 | def workChainConfig = new WorkChainConfig() 203 | workChainConfig.action = actChain 204 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 205 | Registry registry = Registry.of() { r -> 206 | r.add(WorkStatusRepository, workStatusRepository) 207 | r.add(FlowStatusRepository, flowStatusRepository) 208 | r.add(WorkChainConfig, workChainConfig) 209 | r.add(FlowErrorHandler, errorHandler) 210 | r.add(WorkProcessor, processor) 211 | } 212 | 213 | when: 214 | execHarness.run { 215 | processor.onStart(new DefaultEvent(registry, false)) 216 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 217 | processor.start(flowStatus).operation().then() 218 | } 219 | } 220 | 221 | and: 222 | latch.await(10, TimeUnit.SECONDS) 223 | 224 | then: 225 | 0l == latch.count 226 | } 227 | 228 | void "should be able to access the FlowStatus from a work"() { 229 | setup: 230 | CountDownLatch latch = new CountDownLatch(1) 231 | Action actChain = { chain -> chain 232 | .all { ctx -> 233 | FlowStatus status = ctx.get(FlowStatus) 234 | if (status) { 235 | latch.countDown() 236 | } 237 | } 238 | } 239 | FlowErrorHandler errorHandler = { r, f -> latch.countDown() } 240 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 241 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 242 | def workChainConfig = new WorkChainConfig() 243 | workChainConfig.action = actChain 244 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 245 | Registry registry = Registry.of() { r -> 246 | r.add(WorkStatusRepository, workStatusRepository) 247 | r.add(FlowStatusRepository, flowStatusRepository) 248 | r.add(WorkChainConfig, workChainConfig) 249 | r.add(FlowErrorHandler, errorHandler) 250 | r.add(WorkProcessor, processor) 251 | } 252 | 253 | when: 254 | execHarness.run { 255 | processor.onStart(new DefaultEvent(registry, false)) 256 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 257 | processor.start(flowStatus).operation().then() 258 | } 259 | } 260 | 261 | and: 262 | latch.await(10, TimeUnit.SECONDS) 263 | 264 | then: 265 | 0l == latch.count 266 | } 267 | 268 | void "should be able to decorate the chain"() { 269 | setup: 270 | CountDownLatch latch = new CountDownLatch(1) 271 | WorkChainDecorator decorator = { chain -> 272 | chain.all { ctx -> 273 | latch.countDown() 274 | } 275 | } 276 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 277 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 278 | def workChainConfig = new WorkChainConfig() 279 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 280 | Registry registry = Registry.of() { r -> 281 | r.add(WorkStatusRepository, workStatusRepository) 282 | r.add(FlowStatusRepository, flowStatusRepository) 283 | r.add(WorkChainConfig, workChainConfig) 284 | r.add(WorkChainDecorator, decorator) 285 | r.add(WorkProcessor, processor) 286 | } 287 | 288 | when: 289 | execHarness.run { 290 | processor.onStart(new DefaultEvent(registry, false)) 291 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 292 | processor.start(flowStatus).operation().then() 293 | } 294 | } 295 | 296 | and: 297 | latch.await(10, TimeUnit.SECONDS) 298 | 299 | then: 300 | 0l == latch.count 301 | } 302 | 303 | void "should call FlowCompletionHandlers at end of flow"() { 304 | setup: 305 | CountDownLatch latch = new CountDownLatch(1) 306 | Action actChain = { chain -> chain 307 | .all { ctx -> 308 | ctx.complete() 309 | } 310 | } 311 | FlowCompletionHandler completionHandler = { r, f -> 312 | latch.countDown() 313 | } as FlowCompletionHandler 314 | WorkStatusRepository workStatusRepository = new InMemoryWorkStatusRepository() 315 | FlowStatusRepository flowStatusRepository = new InMemoryFlowStatusRepository(workStatusRepository) 316 | def workChainConfig = new WorkChainConfig() 317 | workChainConfig.action = actChain 318 | DefaultWorkProcessor processor = new DefaultWorkProcessor() 319 | Registry registry = Registry.of() { r -> 320 | r.add(WorkStatusRepository, workStatusRepository) 321 | r.add(FlowStatusRepository, flowStatusRepository) 322 | r.add(WorkChainConfig, workChainConfig) 323 | r.add(FlowCompletionHandler, completionHandler) 324 | r.add(WorkProcessor, processor) 325 | } 326 | 327 | when: 328 | execHarness.run { 329 | processor.onStart(new DefaultEvent(registry, false)) 330 | flowStatusRepository.create(FlowConfigSource.of(configData)).then { flowStatus -> 331 | processor.start(flowStatus).operation().then() 332 | } 333 | } 334 | 335 | and: 336 | latch.await(10, TimeUnit.SECONDS) 337 | 338 | then: 339 | 0l == latch.count 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/internal/FlowStatusDeSerializerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal 2 | 3 | import com.danveloper.ratpack.workflow.WorkState 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import spock.lang.Specification 6 | 7 | class FlowStatusDeSerializerSpec extends Specification { 8 | def status = new DefaultFlowStatus().with { s -> 9 | s.id = 1 10 | s.name = "foo" 11 | s.description = "bar" 12 | s.startTime = 123l 13 | s.endTime = 456l 14 | s.state = WorkState.COMPLETED 15 | s.tags = [stack: "prod"] 16 | s.works = [ 17 | new DefaultWorkStatus() 18 | ] 19 | s 20 | } 21 | def mapper = new ObjectMapper() 22 | void "should (de)serialize properly"() { 23 | setup: 24 | def json = mapper.writeValueAsString(status) 25 | 26 | when: 27 | def st = mapper.readValue(json, DefaultFlowStatus) 28 | 29 | then: 30 | st == status 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/internal/InMemoryFlowStatusRepositorySpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal 2 | 3 | import com.danveloper.ratpack.workflow.FlowConfigSource 4 | import com.google.common.io.ByteSource 5 | import ratpack.config.ConfigData 6 | import ratpack.test.exec.ExecHarness 7 | import spock.lang.Specification 8 | 9 | class InMemoryFlowStatusRepositorySpec extends Specification { 10 | 11 | def json = """ 12 | { 13 | "name": "AWESOMEWORKFLOW-001-EL8_MODE", 14 | "description": "My really awesome workflow!", 15 | "tags": { 16 | "stack": "prod", 17 | "phase": "build" 18 | }, 19 | "works": [ 20 | { 21 | "type": "api/resize", 22 | "version": "1.0", 23 | "data": { 24 | "foo": "bar" 25 | } 26 | }, 27 | { 28 | "type": "api/scale", 29 | "version": "1.0", 30 | "data": { 31 | "direction": "up" 32 | } 33 | } 34 | ] 35 | } 36 | """ 37 | def configData = ConfigData.of { d -> 38 | d.json(ByteSource.wrap(json.bytes)).build() 39 | } 40 | def workStatusRepo = new InMemoryWorkStatusRepository() 41 | def flowStatusRepo = new InMemoryFlowStatusRepository(workStatusRepo) 42 | 43 | void "should create work statuses for each work"() { 44 | given: 45 | def status = ExecHarness.yieldSingle { 46 | flowStatusRepo.create(FlowConfigSource.of(configData)) 47 | }.valueOrThrow 48 | 49 | expect: 50 | 2 == status.works.size() 51 | } 52 | 53 | void "should be able to retrieve a flow status from the repo"() { 54 | given: 55 | def status = ExecHarness.yieldSingle { 56 | flowStatusRepo.create(FlowConfigSource.of(configData)) 57 | }.valueOrThrow 58 | 59 | expect: 60 | status == ExecHarness.yieldSingle { 61 | flowStatusRepo.get(status.getId()) 62 | }.valueOrThrow 63 | } 64 | 65 | void "should be able to retrieve flow statuses by tags"() { 66 | setup: 67 | def status = ExecHarness.yieldSingle { 68 | flowStatusRepo.create(FlowConfigSource.of(configData)) 69 | }.valueOrThrow 70 | 71 | when: 72 | def page = ExecHarness.yieldSingle { 73 | flowStatusRepo.findByTag(0, 10, "stack", "prod") 74 | }.valueOrThrow 75 | 76 | and: 77 | def statuses = page.objs 78 | 79 | then: 80 | 1 == statuses.size() 81 | statuses[0] == status 82 | 83 | when: 84 | page = ExecHarness.yieldSingle { 85 | flowStatusRepo.findByTag(0, 10, "phase", "build") 86 | }.valueOrThrow 87 | 88 | and: 89 | statuses = page.objs 90 | 91 | then: 92 | 1 == statuses.size() 93 | statuses[0] == status 94 | 95 | when: 96 | page = ExecHarness.yieldSingle { 97 | flowStatusRepo.findByTag(0, 10, "nothing", "") 98 | }.valueOrThrow 99 | 100 | and: 101 | statuses = page.objs 102 | 103 | then: 104 | 0 == statuses.size() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/internal/TestObject.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal 2 | 3 | class TestObject { 4 | String foo 5 | } 6 | -------------------------------------------------------------------------------- /ratpack-workflow-core/src/test/groovy/com/danveloper/ratpack/workflow/internal/WorkStatusDeSerializerSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal 2 | 3 | import com.danveloper.ratpack.workflow.WorkConfigSource 4 | import com.danveloper.ratpack.workflow.WorkState 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.google.common.io.ByteSource 7 | import ratpack.config.ConfigData 8 | import spock.lang.Specification 9 | 10 | class WorkStatusDeSerializerSpec extends Specification { 11 | 12 | def d = ConfigData.of { d -> d 13 | .json(ByteSource.wrap(""" 14 | { 15 | "type": "api/resize", 16 | "version": "1.0", 17 | "data": { 18 | "foo": "bar" 19 | } 20 | } 21 | """.bytes)) 22 | } 23 | 24 | def status = new DefaultWorkStatus().with { s -> 25 | s.id = "1" 26 | s.startTime = 123l 27 | s.endTime = 456l 28 | s.state = WorkState.COMPLETED 29 | s.config = WorkConfigSource.of(d) 30 | s 31 | } 32 | 33 | def mapper = new ObjectMapper() 34 | 35 | void "should deserialize DefaultWorkStatus"() { 36 | setup: 37 | def json = mapper.writeValueAsString(status) 38 | 39 | when: 40 | def st = mapper.readValue(json, DefaultWorkStatus) 41 | 42 | then: 43 | st == status 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/ratpack-workflow-groovy.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | compile project(":ratpack-workflow-core") 4 | compile "io.ratpack:ratpack-groovy:1.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/GroovyWorkChain.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow; 2 | 3 | import groovy.lang.Closure; 4 | import groovy.lang.DelegatesTo; 5 | 6 | public interface GroovyWorkChain extends WorkChain { 7 | 8 | default GroovyWorkChain all(Work work) { 9 | return (GroovyWorkChain)work("", work::handle); 10 | } 11 | 12 | default GroovyWorkChain all(@DelegatesTo(value = WorkContext.class, strategy = Closure.DELEGATE_FIRST) Closure work) { 13 | Work w = ctx -> { 14 | work.setDelegate(ctx); 15 | work.setResolveStrategy(Closure.DELEGATE_FIRST); 16 | work.call(); 17 | }; 18 | return all(w); 19 | } 20 | 21 | default GroovyWorkChain work(String type, @DelegatesTo(value = WorkContext.class, strategy = Closure.DELEGATE_FIRST) Closure work) { 22 | return work(type, "", work); 23 | } 24 | 25 | GroovyWorkChain work(String type, String version, @DelegatesTo(value = WorkContext.class, strategy = Closure.DELEGATE_FIRST) Closure work); 26 | 27 | GroovyWorkChain work(String type, String version, Class work); 28 | 29 | default GroovyWorkChain flow(String type, @DelegatesTo(value = GroovyWorkChain.class, strategy = Closure.DELEGATE_FIRST) Closure subchain) throws Exception { 30 | return flow(type, "", subchain); 31 | } 32 | 33 | default GroovyWorkChain insert(@DelegatesTo(value = GroovyWorkChain.class, strategy = Closure.DELEGATE_FIRST) Closure chain) throws Exception { 34 | chain.setDelegate(this); 35 | chain.setResolveStrategy(Closure.DELEGATE_FIRST); 36 | chain.call(); 37 | return this; 38 | } 39 | 40 | GroovyWorkChain flow(String type, String version, @DelegatesTo(value = GroovyWorkChain.class, strategy = Closure.DELEGATE_FIRST) Closure subchain) throws Exception; 41 | } 42 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/groovy/GroovyRatpackWorkflowMain.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.groovy; 2 | 3 | import com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow; 4 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow; 5 | 6 | public class GroovyRatpackWorkflowMain { 7 | 8 | public static void main(String[] args) throws Exception { 9 | RatpackWorkflow.start(GroovyRatpackWorkflow.Script.app()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/internal/DefaultGroovyWorkChain.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.GroovyWorkChain; 4 | import com.danveloper.ratpack.workflow.Work; 5 | import com.danveloper.ratpack.workflow.WorkChain; 6 | import com.danveloper.ratpack.workflow.WorkContext; 7 | import groovy.lang.Closure; 8 | import groovy.lang.DelegatesTo; 9 | import ratpack.func.Action; 10 | 11 | import java.util.List; 12 | 13 | public class DefaultGroovyWorkChain implements GroovyWorkChain { 14 | 15 | private WorkChain delegate; 16 | 17 | public DefaultGroovyWorkChain(WorkChain delegate) { 18 | this.delegate = delegate; 19 | } 20 | 21 | @Override 22 | public GroovyWorkChain work(String type, String version, @DelegatesTo(value = WorkContext.class, strategy = Closure.DELEGATE_FIRST) Closure work) { 23 | delegate.work(type, version, ctx -> { 24 | work.setDelegate(ctx); 25 | work.setResolveStrategy(Closure.DELEGATE_FIRST); 26 | work.call(); 27 | }); 28 | return this; 29 | } 30 | 31 | @Override 32 | public WorkChain all(Class work) { 33 | delegate.all(work); 34 | return this; 35 | } 36 | 37 | @Override 38 | public WorkChain work(String type, String version, Work work) { 39 | delegate.work(type, version, work); 40 | return this; 41 | } 42 | 43 | @Override 44 | public GroovyWorkChain work(String type, String version, Class work) { 45 | delegate.work(type, version, work); 46 | return this; 47 | } 48 | 49 | @Override 50 | public GroovyWorkChain flow(String type, String version, @DelegatesTo(value = GroovyWorkChain.class, strategy = Closure.DELEGATE_FIRST) Closure subchain) throws Exception { 51 | delegate.flow(type, version, wc -> { 52 | subchain.setDelegate(new DefaultGroovyWorkChain(wc)); 53 | subchain.setResolveStrategy(Closure.DELEGATE_FIRST); 54 | subchain.call(); 55 | }); 56 | return this; 57 | } 58 | 59 | @Override 60 | public GroovyWorkChain flow(String type, String version, @DelegatesTo(value = GroovyWorkChain.class, strategy = Closure.DELEGATE_FIRST) Action subchain) throws Exception { 61 | delegate.flow(type, version, subchain); 62 | return this; 63 | } 64 | 65 | @Override 66 | public List getWorks() { 67 | return delegate.getWorks(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/internal/StandaloneWorkflowScriptBacking.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal; 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatusRepository; 4 | import com.danveloper.ratpack.workflow.GroovyWorkChain; 5 | import com.danveloper.ratpack.workflow.WorkStatusRepository; 6 | import com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow; 7 | import com.danveloper.ratpack.workflow.server.RatpackWorkflow; 8 | import com.danveloper.ratpack.workflow.server.RatpackWorkflowServerSpec; 9 | import groovy.lang.Closure; 10 | import groovy.lang.DelegatesTo; 11 | import groovy.lang.GroovySystem; 12 | import ratpack.func.Action; 13 | import ratpack.func.Function; 14 | import ratpack.groovy.Groovy; 15 | import ratpack.groovy.internal.ClosureUtil; 16 | import ratpack.groovy.internal.GroovyVersionCheck; 17 | import ratpack.groovy.internal.StandaloneScriptBacking; 18 | import ratpack.guice.Guice; 19 | import ratpack.server.RatpackServer; 20 | import ratpack.server.RatpackServerSpec; 21 | import ratpack.server.ServerConfig; 22 | import ratpack.server.ServerConfigBuilder; 23 | import ratpack.util.Exceptions; 24 | 25 | import java.nio.file.Path; 26 | import java.util.Optional; 27 | import java.util.concurrent.atomic.AtomicReference; 28 | 29 | public class StandaloneWorkflowScriptBacking implements Action> { 30 | private final static AtomicReference> CAPTURE_ACTION = new AtomicReference<>(null); 31 | private final ThreadLocal running = new ThreadLocal<>(); 32 | 33 | public static void captureNext(Action action) { 34 | CAPTURE_ACTION.set(action); 35 | } 36 | 37 | public void execute(final Closure closure) throws Exception { 38 | GroovyVersionCheck.ensureRequiredVersionUsed(GroovySystem.getVersion()); 39 | 40 | Optional.ofNullable(running.get()).ifPresent(s -> Exceptions.uncheck(s::stop)); 41 | 42 | RatpackServer ratpackServer; 43 | Path scriptFile = ClosureUtil.findScript(closure); 44 | if (scriptFile == null) { 45 | ratpackServer = RatpackWorkflow.of(server -> ClosureUtil.configureDelegateFirst(new RatpackBacking(server), closure)); 46 | } else { 47 | ratpackServer = RatpackWorkflow.of(GroovyRatpackWorkflow.Script.app(scriptFile)); 48 | Action action = CAPTURE_ACTION.getAndSet(null); 49 | if (action != null) { 50 | action.execute(ratpackServer); 51 | } 52 | } 53 | 54 | ratpackServer.start(); 55 | running.set(ratpackServer); 56 | } 57 | 58 | private static class RatpackBacking implements GroovyRatpackWorkflow.Ratpack { 59 | private final RatpackWorkflowServerSpec server; 60 | 61 | public RatpackBacking(RatpackWorkflowServerSpec server) { 62 | this.server = server; 63 | } 64 | 65 | @Override 66 | public void bindings(Closure configurer) { 67 | server.registry(Guice.registry(ClosureUtil.delegatingAction(configurer))); 68 | } 69 | 70 | @Override 71 | public void handlers(Closure configurer) { 72 | Exceptions.uncheck(() -> server.handlers(Groovy.chainAction(configurer))); 73 | } 74 | 75 | @Override 76 | public void serverConfig(Closure configurer) { 77 | ServerConfigBuilder builder = ServerConfig.builder().development(true); 78 | ClosureUtil.configureDelegateFirst(builder, configurer); 79 | server.serverConfig(builder); 80 | } 81 | 82 | @Override 83 | public void workflow(Closure configurer) { 84 | Exceptions.uncheck(() -> server.workflow(ClosureUtil.delegatingAction(configurer))); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/internal/capture/RatpackWorkflowDslBacking.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal.capture; 2 | 3 | import com.danveloper.ratpack.workflow.FlowStatusRepository; 4 | import com.danveloper.ratpack.workflow.WorkStatusRepository; 5 | import com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow; 6 | import groovy.lang.Closure; 7 | import ratpack.func.Function; 8 | 9 | public class RatpackWorkflowDslBacking implements GroovyRatpackWorkflow.Ratpack { 10 | private RatpackWorkflowDslClosures closures; 11 | 12 | public RatpackWorkflowDslBacking(RatpackWorkflowDslClosures closures) { 13 | this.closures = closures; 14 | } 15 | 16 | @Override 17 | public void bindings(Closure configurer) { 18 | closures.setBindings(configurer); 19 | } 20 | 21 | @Override 22 | public void handlers(Closure configurer) { 23 | closures.setHandlers(configurer); 24 | } 25 | 26 | @Override 27 | public void serverConfig(Closure configurer) { 28 | closures.setServerConfig(configurer); 29 | } 30 | 31 | @Override 32 | public void workflow(Closure configurer) { 33 | closures.setWorkflows(configurer); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/internal/capture/RatpackWorkflowDslClosures.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal.capture; 2 | 3 | import com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow; 4 | import groovy.lang.Closure; 5 | import ratpack.func.Block; 6 | import ratpack.func.Function; 7 | import ratpack.groovy.internal.ClosureUtil; 8 | import ratpack.groovy.internal.capture.RatpackScriptBacking; 9 | 10 | public class RatpackWorkflowDslClosures { 11 | 12 | private Closure workflows; 13 | private Closure handlers; 14 | private Closure bindings; 15 | private Closure serverConfig; 16 | 17 | public Closure getHandlers() { 18 | return handlers; 19 | } 20 | 21 | public Closure getBindings() { 22 | return bindings; 23 | } 24 | 25 | public Closure getServerConfig() { 26 | return serverConfig; 27 | } 28 | 29 | public Closure getWorkflows() { 30 | return workflows; 31 | } 32 | 33 | public void setHandlers(Closure handlers) { 34 | this.handlers = handlers; 35 | } 36 | 37 | public void setBindings(Closure bindings) { 38 | this.bindings = bindings; 39 | } 40 | 41 | public void setServerConfig(Closure serverConfig) { 42 | this.serverConfig = serverConfig; 43 | } 44 | 45 | public void setWorkflows(Closure workflows) { 46 | this.workflows = workflows; 47 | } 48 | 49 | public static RatpackWorkflowDslClosures capture(Function function, Block action) throws Exception { 50 | RatpackWorkflowDslClosures closures = new RatpackWorkflowDslClosures(); 51 | GroovyRatpackWorkflow.Ratpack receiver = function.apply(closures); 52 | RatpackScriptBacking.withBacking(closure -> ClosureUtil.configureDelegateFirst(receiver, closure), action); 53 | return closures; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ratpack-workflow-groovy/src/main/java/com/danveloper/ratpack/workflow/internal/capture/RatpackWorkflowDslScriptCapture.java: -------------------------------------------------------------------------------- 1 | package com.danveloper.ratpack.workflow.internal.capture; 2 | 3 | import com.danveloper.ratpack.workflow.server.GroovyRatpackWorkflow; 4 | import groovy.lang.Script; 5 | import ratpack.func.BiFunction; 6 | import ratpack.func.Function; 7 | import ratpack.groovy.script.internal.ScriptEngine; 8 | 9 | import java.nio.file.Path; 10 | 11 | public class RatpackWorkflowDslScriptCapture implements BiFunction { 12 | private final boolean compileStatic; 13 | private final Function function; 14 | 15 | public RatpackWorkflowDslScriptCapture(boolean compileStatic, 16 | Function function) { 17 | this.compileStatic = compileStatic; 18 | this.function = function; 19 | } 20 | 21 | public RatpackWorkflowDslClosures apply(Path file, String script) throws Exception { 22 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 23 | ScriptEngine