Initial import
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
build
|
||||||
|
.gradle
|
||||||
|
.idea
|
||||||
|
.lazybones
|
||||||
|
**/*.iml
|
||||||
|
out
|
||||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
Ratpack project template
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
You have just created a basic Groovy Ratpack application. It doesn't do much
|
||||||
|
at this point, but we have set you up with a standard project structure, a
|
||||||
|
Guice back Registry, simple home page, and Spock for writing tests (because
|
||||||
|
you'd be mad not to use it).
|
||||||
|
|
||||||
|
In this project you get:
|
||||||
|
|
||||||
|
* A Gradle build file with pre-built Gradle wrapper
|
||||||
|
* A tiny home page at src/ratpack/templates/index.html (it's a template)
|
||||||
|
* A routing file at src/ratpack/Ratpack.groovy
|
||||||
|
* Reloading enabled in build.gradle
|
||||||
|
* A standard project structure:
|
||||||
|
|
||||||
|
<proj>
|
||||||
|
|
|
||||||
|
+- src
|
||||||
|
|
|
||||||
|
+- ratpack
|
||||||
|
| |
|
||||||
|
| +- Ratpack.groovy
|
||||||
|
| +- ratpack.properties
|
||||||
|
| +- public // Static assets in here
|
||||||
|
| |
|
||||||
|
| +- images
|
||||||
|
| +- lib
|
||||||
|
| +- scripts
|
||||||
|
| +- styles
|
||||||
|
|
|
||||||
|
+- main
|
||||||
|
| |
|
||||||
|
| +- groovy
|
||||||
|
|
|
||||||
|
+- // App classes in here!
|
||||||
|
|
|
||||||
|
+- test
|
||||||
|
|
|
||||||
|
+- groovy
|
||||||
|
|
|
||||||
|
+- // Spock tests in here!
|
||||||
|
|
||||||
|
That's it! You can start the basic app with
|
||||||
|
|
||||||
|
./gradlew run
|
||||||
|
|
||||||
|
but it's up to you to add the bells, whistles, and meat of the application.
|
||||||
37
build.gradle
Normal file
37
build.gradle
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "io.ratpack:ratpack-gradle:1.5.1"
|
||||||
|
classpath 'com.h2database:h2:1.4.191'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "org.flywaydb.flyway" version "5.0.7"
|
||||||
|
id "io.ratpack.ratpack-groovy" version "1.5.1"
|
||||||
|
id 'idea'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile ratpack.dependency('h2')
|
||||||
|
compile ratpack.dependency('jdbc-tx')
|
||||||
|
compile 'org.flywaydb:flyway-core:5.0.7'
|
||||||
|
runtime 'org.slf4j:slf4j-simple:1.7.25'
|
||||||
|
|
||||||
|
testCompile ratpack.dependency('test')
|
||||||
|
testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
|
||||||
|
testCompile 'cglib:cglib:2.2.2'
|
||||||
|
testCompile 'org.objenesis:objenesis:2.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
flyway {
|
||||||
|
url = 'jdbc:h2:mem:dbtdb'
|
||||||
|
user = 'SA'
|
||||||
|
}
|
||||||
|
run.dependsOn(flywayMigrate)
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#Fri Feb 23 08:49:01 EET 2018
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip
|
||||||
160
gradlew
vendored
Executable file
160
gradlew
vendored
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn ( ) {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die ( ) {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||||
|
function splitJvmOpts() {
|
||||||
|
JVM_OPTS=("$@")
|
||||||
|
}
|
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||||
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windowz variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
goto execute
|
||||||
|
|
||||||
|
:4NT_args
|
||||||
|
@rem Get arguments from the 4NT Shell from JP Software
|
||||||
|
set CMD_LINE_ARGS=%$
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
62
src/main/groovy/com/devsoap/dbt/BlockTransaction.groovy
Normal file
62
src/main/groovy/com/devsoap/dbt/BlockTransaction.groovy
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package com.devsoap.dbt
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class BlockTransaction implements Serializable {
|
||||||
|
|
||||||
|
String id = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
List<Query> queries = []
|
||||||
|
|
||||||
|
boolean completed = false
|
||||||
|
|
||||||
|
boolean executed = false
|
||||||
|
|
||||||
|
boolean rolledback = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a query in the transaction
|
||||||
|
*
|
||||||
|
* @param query
|
||||||
|
* the query to execute
|
||||||
|
* @return
|
||||||
|
* the result of the query
|
||||||
|
*/
|
||||||
|
void execute(String query) {
|
||||||
|
queries << new Query(queries.empty? null : queries.last(), query)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End the current transaction
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
void end() {
|
||||||
|
completed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// A block in the chain
|
||||||
|
static final class Query implements Serializable {
|
||||||
|
String data
|
||||||
|
String hash
|
||||||
|
long timeStamp
|
||||||
|
Object result
|
||||||
|
|
||||||
|
Query() {
|
||||||
|
// For serialization
|
||||||
|
}
|
||||||
|
|
||||||
|
Query(Query previous, String data) {
|
||||||
|
this.data = data
|
||||||
|
timeStamp = new Date().getTime()
|
||||||
|
hash = generateHash(previous?.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String generateHash(String previousHash) {
|
||||||
|
def digest = MessageDigest.getInstance('SHA-256')
|
||||||
|
def hash = digest.digest("${previousHash?:''}$timeStamp$data".getBytes(StandardCharsets.UTF_8))
|
||||||
|
hash.encodeHex().toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/main/groovy/com/devsoap/dbt/DBTManager.groovy
Normal file
112
src/main/groovy/com/devsoap/dbt/DBTManager.groovy
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package com.devsoap.dbt
|
||||||
|
|
||||||
|
import com.devsoap.dbt.config.DBTConfig
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import ratpack.exec.Promise
|
||||||
|
import ratpack.http.client.HttpClient
|
||||||
|
import ratpack.service.Service
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class DBTManager implements Service {
|
||||||
|
|
||||||
|
private final String ledgerUrl
|
||||||
|
private final HttpClient httpClient
|
||||||
|
private final ObjectMapper mapper
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DBTManager(DBTConfig config, HttpClient httpClient, ObjectMapper mapper){
|
||||||
|
ledgerUrl = config.ledger.url
|
||||||
|
this.httpClient = httpClient
|
||||||
|
this.mapper = mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise<JsonNode> execute(ExecuteQuery queryBuilder) {
|
||||||
|
log.info("Executing new transaction")
|
||||||
|
def builder = new TransactionBuilder(this)
|
||||||
|
queryBuilder.build(builder)
|
||||||
|
def transaction = builder.build()
|
||||||
|
|
||||||
|
log.info("Sending transaction $transaction.id to ledger")
|
||||||
|
httpClient.post(ledgerUrl.toURI(), { spec ->
|
||||||
|
spec.body.text(mapper.writeValueAsString(transaction))
|
||||||
|
}).flatMap { response ->
|
||||||
|
Promise.value(mapper.readTree(response.body.text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise<JsonNode> execute(String transactionId, ExecuteQuery queryBuilder) {
|
||||||
|
log.info("Amending existing transaction $transactionId")
|
||||||
|
|
||||||
|
log.info("Getting transaction $transactionId from ledger")
|
||||||
|
httpClient.get(ledgerUrl.toURI(), { spec ->
|
||||||
|
spec.headers.add('X-Transaction-Id', transactionId)
|
||||||
|
}).flatMap { response ->
|
||||||
|
def oldTransaction = mapper.readValue(response.body.text, BlockTransaction)
|
||||||
|
if(oldTransaction == null) {
|
||||||
|
throw new RuntimeException("Transaction with id $transactionId could not be found")
|
||||||
|
}
|
||||||
|
if(oldTransaction.completed) {
|
||||||
|
throw new RuntimeException("Cannot modify a completed transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
def builder = new TransactionBuilder(this, oldTransaction)
|
||||||
|
queryBuilder.build(builder)
|
||||||
|
def transaction = builder.build()
|
||||||
|
|
||||||
|
if(transaction.id != transactionId) {
|
||||||
|
throw new RuntimeException("Transaction id changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Sending transaction $transactionId to ledger")
|
||||||
|
httpClient.post(ledgerUrl.toURI(), { spec ->
|
||||||
|
spec.body.text(mapper.writeValueAsString(transaction))
|
||||||
|
})
|
||||||
|
}.flatMap { response ->
|
||||||
|
Promise.value(mapper.readTree(response.body.text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransactionBuilder {
|
||||||
|
|
||||||
|
private final List<String> queries = []
|
||||||
|
|
||||||
|
private final DBTManager manager
|
||||||
|
|
||||||
|
private final BlockTransaction transaction
|
||||||
|
|
||||||
|
private TransactionBuilder(DBTManager manager){
|
||||||
|
this(manager, new BlockTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransactionBuilder(DBTManager manager, BlockTransaction transaction) {
|
||||||
|
this.manager = manager
|
||||||
|
this.transaction = transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
void query(String sql){
|
||||||
|
queries << sql
|
||||||
|
}
|
||||||
|
|
||||||
|
String id() {
|
||||||
|
transaction.id
|
||||||
|
}
|
||||||
|
|
||||||
|
void complete() {
|
||||||
|
transaction.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockTransaction build() {
|
||||||
|
queries.each { transaction.execute(it) }
|
||||||
|
transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ExecuteQuery {
|
||||||
|
void build(TransactionBuilder builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/groovy/com/devsoap/dbt/app/DatabaseService.groovy
Normal file
15
src/main/groovy/com/devsoap/dbt/app/DatabaseService.groovy
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.devsoap.dbt.app
|
||||||
|
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import ratpack.service.Service
|
||||||
|
import ratpack.service.StartEvent
|
||||||
|
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
class DatabaseService implements Service {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onStart(StartEvent event){
|
||||||
|
new Flyway(dataSource: event.registry.get(DataSource)).migrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main/groovy/com/devsoap/dbt/config/DBTConfig.groovy
Normal file
5
src/main/groovy/com/devsoap/dbt/config/DBTConfig.groovy
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.devsoap.dbt.config
|
||||||
|
|
||||||
|
class DBTConfig {
|
||||||
|
LedgerConfig ledger = new LedgerConfig()
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.devsoap.dbt.config
|
||||||
|
|
||||||
|
class LedgerConfig {
|
||||||
|
String url = 'http://localhost:5050/ledger'
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.devsoap.dbt.handlers
|
||||||
|
|
||||||
|
import com.devsoap.dbt.BlockTransaction
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import ratpack.exec.Promise
|
||||||
|
import ratpack.handling.Context
|
||||||
|
import ratpack.handling.Handler
|
||||||
|
import ratpack.http.Status
|
||||||
|
import ratpack.jdbctx.Transaction
|
||||||
|
|
||||||
|
import javax.sql.DataSource
|
||||||
|
import java.sql.ResultSet
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class ExecutorHandler implements Handler {
|
||||||
|
|
||||||
|
static final String PATH = 'executor'
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void handle(Context ctx) throws Exception {
|
||||||
|
ctx.request.body.then { body ->
|
||||||
|
def mapper = ctx.get(ObjectMapper)
|
||||||
|
def ds = ctx.get(DataSource)
|
||||||
|
def transaction = mapper.readValue(body.text, BlockTransaction)
|
||||||
|
|
||||||
|
if(!validateChain(transaction)) {
|
||||||
|
ctx.response.status = Status.of(400, 'Transaction chain invalid')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommands(ds, mapper, transaction).then {
|
||||||
|
transaction.executed = true
|
||||||
|
ctx.response.send(mapper.writeValueAsString(transaction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean validateChain(BlockTransaction transaction) {
|
||||||
|
//FIXME
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise<BlockTransaction> executeCommands(DataSource ds, ObjectMapper mapper, BlockTransaction transaction) {
|
||||||
|
def txDs = Transaction.dataSource(ds)
|
||||||
|
def tx = Transaction.create { ds.connection }
|
||||||
|
tx.wrap {
|
||||||
|
Promise.sync {
|
||||||
|
transaction.queries.each { block ->
|
||||||
|
log.info "Executing $block.data ..."
|
||||||
|
if(block.data.toLowerCase().startsWith("select")){
|
||||||
|
def result = txDs.connection
|
||||||
|
.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)
|
||||||
|
.executeQuery(block.data)
|
||||||
|
block.result = toJson(mapper, result)
|
||||||
|
} else {
|
||||||
|
txDs.connection.createStatement().execute(block.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonNode toJson(ObjectMapper mapper, ResultSet resultSet) {
|
||||||
|
def json = mapper.createObjectNode()
|
||||||
|
|
||||||
|
if(resultSet.last()) {
|
||||||
|
int rows = resultSet.row
|
||||||
|
log.info("Converting $rows rows to json")
|
||||||
|
resultSet.beforeFirst()
|
||||||
|
|
||||||
|
resultSet.metaData.columnCount.times { column ->
|
||||||
|
def columnIndex = column + 1
|
||||||
|
def columnName = resultSet.metaData.getColumnName(columnIndex)
|
||||||
|
ArrayNode columnValue = json.get(columnName)
|
||||||
|
if(!columnValue) {
|
||||||
|
columnValue = mapper.createArrayNode()
|
||||||
|
json.set(columnName, columnValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSet.beforeFirst()
|
||||||
|
while(resultSet.next()) {
|
||||||
|
columnValue.addPOJO(resultSet.getObject(columnIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//mapper.writeValueAsString(json)
|
||||||
|
mapper.valueToTree(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.devsoap.dbt.handlers
|
||||||
|
|
||||||
|
import com.devsoap.dbt.BlockTransaction
|
||||||
|
import com.devsoap.dbt.services.LedgerService
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import ratpack.handling.Context
|
||||||
|
import ratpack.handling.Handler
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class LedgerHandler implements Handler {
|
||||||
|
|
||||||
|
static final String PATH = 'ledger'
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void handle(Context ctx) {
|
||||||
|
def ledgerService = ctx.get(LedgerService)
|
||||||
|
|
||||||
|
ctx.byMethod {
|
||||||
|
delegate = it
|
||||||
|
|
||||||
|
get({
|
||||||
|
def transaction = ledgerService.fetchTransaction(ctx.request.headers['X-Transaction-Id'].toString())
|
||||||
|
def mapper = ctx.get(ObjectMapper)
|
||||||
|
ctx.response.send(mapper.writeValueAsString(transaction))
|
||||||
|
} as Handler)
|
||||||
|
|
||||||
|
post({
|
||||||
|
ctx.request.body.then { body ->
|
||||||
|
def mapper = ctx.get(ObjectMapper)
|
||||||
|
def transaction = mapper.readValue(body.text, BlockTransaction)
|
||||||
|
|
||||||
|
def existingTransaction = ledgerService.fetchTransaction(transaction.id)
|
||||||
|
if(existingTransaction) {
|
||||||
|
ledgerService.updateTransaction(transaction)
|
||||||
|
} else {
|
||||||
|
log.info("Creating new transaction")
|
||||||
|
ledgerService.newTransaction(transaction)
|
||||||
|
}
|
||||||
|
if(transaction.completed){
|
||||||
|
log.info("Sending transaction $transaction.id to executor")
|
||||||
|
ctx.redirect(ExecutorHandler.PATH)
|
||||||
|
} else {
|
||||||
|
ctx.response.send(mapper.writeValueAsString(transaction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as Handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.devsoap.dbt.services
|
||||||
|
|
||||||
|
import com.devsoap.dbt.BlockTransaction
|
||||||
|
import groovy.util.logging.Log
|
||||||
|
import ratpack.service.Service
|
||||||
|
import ratpack.service.StartEvent
|
||||||
|
import ratpack.service.StopEvent
|
||||||
|
|
||||||
|
@Log
|
||||||
|
class LedgerService implements Service {
|
||||||
|
|
||||||
|
static final transient List<BlockTransaction> transactions = []
|
||||||
|
|
||||||
|
BlockTransaction fetchTransaction(String transactionId) {
|
||||||
|
log.info("Fetching transaction $transactionId")
|
||||||
|
log.info("Transactions:$transactions")
|
||||||
|
transactions.find {it.id == transactionId}
|
||||||
|
}
|
||||||
|
|
||||||
|
String newTransaction(BlockTransaction transaction) {
|
||||||
|
log.info("Adding new transaction $transaction.id")
|
||||||
|
transactions << transaction
|
||||||
|
transaction.id
|
||||||
|
}
|
||||||
|
|
||||||
|
String updateTransaction(BlockTransaction transaction) {
|
||||||
|
log.info("Updating transaction $transaction.id")
|
||||||
|
def existingTransaction = fetchTransaction(transaction.id)
|
||||||
|
def index = transactions.indexOf(existingTransaction)
|
||||||
|
transactions.remove(index)
|
||||||
|
transactions.add(index, transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/main/resources/db/migration/V1__create_tbl_logs.sql
Normal file
4
src/main/resources/db/migration/V1__create_tbl_logs.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
create table LOGS (
|
||||||
|
LOG_ID int not null,
|
||||||
|
LOG_VALUE varchar(100) not null
|
||||||
|
);
|
||||||
75
src/ratpack/Ratpack.groovy
Normal file
75
src/ratpack/Ratpack.groovy
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import com.devsoap.dbt.app.DatabaseService
|
||||||
|
import com.devsoap.dbt.config.DBTConfig
|
||||||
|
import com.devsoap.dbt.DBTManager
|
||||||
|
import com.devsoap.dbt.handlers.ExecutorHandler
|
||||||
|
import com.devsoap.dbt.handlers.LedgerHandler
|
||||||
|
import com.devsoap.dbt.services.LedgerService
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.h2.jdbcx.JdbcDataSource
|
||||||
|
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
import static ratpack.groovy.Groovy.ratpack
|
||||||
|
|
||||||
|
ratpack {
|
||||||
|
|
||||||
|
serverConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DBT Framework config
|
||||||
|
*/
|
||||||
|
require("", DBTConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings {
|
||||||
|
bindInstance(new ObjectMapper())
|
||||||
|
bindInstance(DataSource, new JdbcDataSource(url: 'jdbc:h2:mem:dbtdb;DB_CLOSE_DELAY=-1', user: ''))
|
||||||
|
bindInstance(new DatabaseService())
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DBT Framework manager
|
||||||
|
*/
|
||||||
|
bind(DBTManager)
|
||||||
|
bind(LedgerService)
|
||||||
|
bind(ExecutorHandler)
|
||||||
|
bind(LedgerHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DBT Framework handlers
|
||||||
|
*/
|
||||||
|
path('executor', ExecutorHandler)
|
||||||
|
path('ledger', LedgerHandler)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumer services
|
||||||
|
*/
|
||||||
|
get('frontend') {
|
||||||
|
get(DBTManager).execute { transaction ->
|
||||||
|
transaction.query("INSERT INTO LOGS(LOG_ID,LOG_VALUE) VALUES (${new Random().nextInt()}, 'HELLO')")
|
||||||
|
}.then {
|
||||||
|
redirect("/gateway/${it['id'].textValue()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get('gateway/:transactionId?') {
|
||||||
|
get(DBTManager).execute(pathTokens.transactionId, { transaction ->
|
||||||
|
transaction.query("INSERT INTO LOGS(LOG_ID,LOG_VALUE) VALUES (${new Random().nextInt()}, 'WORLD')")
|
||||||
|
}).then {
|
||||||
|
redirect("/gateway2/${it['id'].textValue()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get('gateway2/:transactionId?') {
|
||||||
|
get(DBTManager).execute(pathTokens.transactionId, { transaction ->
|
||||||
|
transaction.query("SELECT * FROM LOGS")
|
||||||
|
transaction.complete()
|
||||||
|
}).then {
|
||||||
|
render it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.devsoap.dbt.framework
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
|
||||||
|
import spock.lang.AutoCleanup
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class ExecutorSpec extends Specification {
|
||||||
|
|
||||||
|
def mapper = new ObjectMapper()
|
||||||
|
|
||||||
|
@AutoCleanup
|
||||||
|
def aut = new GroovyRatpackMainApplicationUnderTest()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
78
src/test/groovy/com/devsoap/dbt/framework/LedgerSpec.groovy
Normal file
78
src/test/groovy/com/devsoap/dbt/framework/LedgerSpec.groovy
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package com.devsoap.dbt.framework
|
||||||
|
|
||||||
|
import com.devsoap.dbt.BlockTransaction
|
||||||
|
import com.devsoap.dbt.handlers.LedgerHandler
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
|
||||||
|
import spock.lang.AutoCleanup
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class LedgerSpec extends Specification {
|
||||||
|
|
||||||
|
def mapper = new ObjectMapper()
|
||||||
|
def jsonSlurper = new JsonSlurper()
|
||||||
|
|
||||||
|
@AutoCleanup
|
||||||
|
def aut = new GroovyRatpackMainApplicationUnderTest()
|
||||||
|
|
||||||
|
void 'transaction sent to ledger'() {
|
||||||
|
setup:
|
||||||
|
def transaction = new BlockTransaction()
|
||||||
|
transaction.execute("SELECT * FROM LOGS")
|
||||||
|
when:
|
||||||
|
def response = mapper.readValue(aut.httpClient.requestSpec { spec ->
|
||||||
|
spec.body.text(mapper.writeValueAsString(transaction))
|
||||||
|
}.post(LedgerHandler.PATH).body.text, BlockTransaction)
|
||||||
|
then:
|
||||||
|
response.id == transaction.id
|
||||||
|
response.completed == false
|
||||||
|
}
|
||||||
|
|
||||||
|
void 'completed transaction marked as completed'() {
|
||||||
|
setup:
|
||||||
|
def transaction = new BlockTransaction()
|
||||||
|
transaction.execute("SELECT * FROM LOGS")
|
||||||
|
transaction.end()
|
||||||
|
when:
|
||||||
|
def response = mapper.readValue(aut.httpClient.requestSpec { spec ->
|
||||||
|
spec.body.text(mapper.writeValueAsString(transaction))
|
||||||
|
}.post(LedgerHandler.PATH).body.text, BlockTransaction)
|
||||||
|
then:
|
||||||
|
response.id == transaction.id
|
||||||
|
response.completed == true
|
||||||
|
}
|
||||||
|
|
||||||
|
void 'completed transaction sent to executor from ledger'() {
|
||||||
|
setup:
|
||||||
|
def transaction = new BlockTransaction()
|
||||||
|
transaction.execute("SELECT * FROM LOGS")
|
||||||
|
transaction.end()
|
||||||
|
when:
|
||||||
|
def response = mapper.readValue(aut.httpClient.requestSpec { spec ->
|
||||||
|
spec.body.text(mapper.writeValueAsString(transaction))
|
||||||
|
}.post(LedgerHandler.PATH).body.text, BlockTransaction)
|
||||||
|
then:
|
||||||
|
response.id == transaction.id
|
||||||
|
response.executed == true
|
||||||
|
response.rolledback == false
|
||||||
|
response.completed == true
|
||||||
|
}
|
||||||
|
|
||||||
|
void 'result is attached to block and executed in order'() {
|
||||||
|
setup:
|
||||||
|
def transaction = new BlockTransaction()
|
||||||
|
transaction.execute("INSERT INTO LOGS(LOG_ID,LOG_VALUE) VALUES (1, 'HELLO')")
|
||||||
|
transaction.execute("SELECT * FROM LOGS")
|
||||||
|
transaction.end()
|
||||||
|
when:
|
||||||
|
def response = mapper.readValue(aut.httpClient.requestSpec { spec ->
|
||||||
|
spec.body.text(mapper.writeValueAsString(transaction))
|
||||||
|
}.post(LedgerHandler.PATH).body.text, BlockTransaction)
|
||||||
|
def json = jsonSlurper.parseText(response.queries[1].result)
|
||||||
|
then:
|
||||||
|
response.id == transaction.id
|
||||||
|
json.LOG_ID.first() == 1
|
||||||
|
json.LOG_VALUE.first() == 'HELLO'
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user