diff --git a/todo-template/README.md b/todo-template/README.md new file mode 100644 index 0000000..8c4259c --- /dev/null +++ b/todo-template/README.md @@ -0,0 +1,46 @@ +# Caplin Todo List Adapter Template + +This project provides a starting point for writing integration Adapters based on the Caplin DataSource Java API. The build is written in gradle and requires either a local installation of gradle or an internet connection over which gradle can be downloaded. This is done seamlessly with the provided `gradlew` script files. + +## Getting started +This section outlines the basic steps necessary to build, deploy and start the Adapter. + +1. download/clone the repository +2. change the project name in settings.gradle. This will be used as the name for the artifact. +3. change the username and password in the `build.gradle` file to your caplin credentials +4. run `gradle assemble` +5. deploy the zip file created in `build/distributions/` into your Deployment Framework +6. configure the host for the deployed blade with the `./dfw hosts` command +7. start the adapter with `./dfw start ` + + +## Development modes +In addition to creating an adapter blade that can be deployed using the Caplin Deployment Framework the build has two more tasks that make it easier to run the adapter from an IDE. The two modes modes differ **run against a local DFW** and **run against a remote DFW** and are explained in more detail in the following two sections. + +### Connect an adapter from an IDE to a local DFW +In order for the Liberator and Transformer to know abou the blade they need the configuration bundled with this blade. The concept of a **config only blade** is essentially a blade that contains only configuration and no binary. When this blade is deployed, the difference to a full blade is that `./dfw start` will not start up the adapter. At this point the integration adapter can be started from the IDE. The following bullet points outline the recommended way of setting this up. + +1. run `gradle assemble -PconfigOnly` +2. deploy the zip file created in `build/distributions/` into your Deployment Framework +3. create a run configuration for the main class of your project + * set the environment variable `CONFIG_BASE` to point to the `global_config/` folder in your DFW + * set the working directory to `/active_blades//DataSource ` +4. Run the adapter using the run configuration just created + +### Connect an adapter from an IDE to a remote DFW +Sometimes, the Liberator and Transformer will not be on the host that is being developed on. In this case the above approach will not work. To solve this issue the build has a task `createWorkingDirectory` that generates necessary (limited) configuration to allow the adapter to connect to a remote host. The following steps are required to set this up. + +1. run `gradle assemble -PconfigOnly` +2. deploy the zip file created in `build/distributions/` into your Deployment Framework (remote host) +3. run `gradle setupWorkingDirectory` which will create the folder `build/env` for use as a working directory. This will also require some default properties to be overridden with the -P command line switch. See the [Gradle Documentation]( https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_properties_and_system_properties). Supported options are: + * thisLeg - defaults to `1` and only needs to be changed if you want to connect to the failover leg + * liberatorHost - defaults to `localhost` and will need to be changed to the host Liberator runs on + * liberatorPort - defaults to `15001` and might need to be changed to the Liberator datasrc port + * transformerHost - defaults to `localhost` and will need to be changed to the host Transformer runs on + * transformerPort - defaults to `15002` and might need to be changed to the Transformer datasrc port +4. Once the working directory has been created check the generated file `build/env/blade_config/environment-ide.conf` for correctnes of generated arguments +5. Create a run configuration with the directory `build/env/DataSource` as a working directory +6. Start the Adapter using the run configuration just created + +## Issues +For issues with the templates please contact Caplin Support or raise an issue on Github. diff --git a/todo-template/blade/DataSource/bin/start-jar.sh b/todo-template/blade/DataSource/bin/start-jar.sh new file mode 100644 index 0000000..da85bb9 --- /dev/null +++ b/todo-template/blade/DataSource/bin/start-jar.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Start the TodoTemplateAdapter Java DataSource. +# +# $1 - Path to java executable +# $2 - Path to datasource XML file +# $3 - Path to fields XML file +# $4 - Java definitions ( optional ) +# +# Returns the process id of the Java process. +# + +BLADENAME=@adapterName@ + +if [ "$1" = "CONFREADER" ]; then + shift + confreading=1 + jar=`ls "$BINARY_ROOT"/lib/datasource-java*.jar|head -1` +else + confreading=0 + jar=`ls "$BINARY_ROOT"/lib/$BLADENAME*.jar|head -1` + classpath="${BINARY_ROOT}/lib/*" + + echo "Classpath: $jar" +fi + +if [ $confreading = 1 ]; then + java -jar "$jar" "$@" + exit $? +else + java -cp "$classpath" -jar "$jar" "$@" > "$LOGDIR"/java-$BLADENAME.log 2>&1 & + echo $! +fi diff --git a/todo-template/blade/DataSource/etc/datasource.conf b/todo-template/blade/DataSource/etc/datasource.conf new file mode 100644 index 0000000..6dc2dfc --- /dev/null +++ b/todo-template/blade/DataSource/etc/datasource.conf @@ -0,0 +1,78 @@ +################################################## +# +# Include base configuration +# +if "${ENV:CONFIG_BASE}x" != "x" + include-file ${ENV:CONFIG_BASE}bootstrap.conf + include-file ${ENV:CONFIG_BASE}fields.conf +endif + +include-file ${ccd}/../../blade_config/bootstrap.conf +include-file ${ccd}/../../blade_config/fields.conf + +datasrc-name ${ADAPTER_NAME}${THIS_LEG}-%h + +################################################## +# +# Logging configuration +# +log-dir %r/var +log-level INFO + +log-use-parent-handlers TRUE + +event-log event-${ADAPTER_NAME}.log + +################################################## +# +# Local DataSource peer-id +# +datasrc-local-label ${ADAPTER_NAME}${THIS_LEG} + +################################################## +# +# Liberator peer +# +add-peer + local-type active|contrib + remote-name liberator${THIS_LEG} + addr ${LIBERATOR${THIS_LEG}_HOST} + port ${LIBERATOR${THIS_LEG}_DATASRCPORT} + heartbeat-time 15 + heartbeat-slack-time 5 +end-peer + +if "${FAILOVER}" == "ENABLED" + add-peer + local-type active|contrib + remote-name liberator${OTHER_LEG} + addr ${LIBERATOR${OTHER_LEG}_HOST} + port ${LIBERATOR${OTHER_LEG}_DATASRCPORT} + heartbeat-time 15 + heartbeat-slack-time 5 + end-peer +endif + +################################################## +# +# Transformer peer +# +add-peer + local-type active|contrib + remote-name transformer${THIS_LEG} + addr ${TRANSFORMER${THIS_LEG}_HOST} + port ${TRANSFORMER${THIS_LEG}_DATASRCPORT} + heartbeat-time 15 + heartbeat-slack-time 5 +end-peer + +if "${FAILOVER}" == "ENABLED" + add-peer + local-type active|contrib + remote-name transformer${OTHER_LEG} + addr ${TRANSFORMER${OTHER_LEG}_HOST} + port ${TRANSFORMER${OTHER_LEG}_DATASRCPORT} + heartbeat-time 15 + heartbeat-slack-time 5 + end-peer +endif diff --git a/todo-template/blade/DataSource/var/.gitkeep b/todo-template/blade/DataSource/var/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/todo-template/blade/Liberator/etc/rttpd.conf b/todo-template/blade/Liberator/etc/rttpd.conf new file mode 100644 index 0000000..1530561 --- /dev/null +++ b/todo-template/blade/Liberator/etc/rttpd.conf @@ -0,0 +1,59 @@ +include-file ${ccd}/../../blade_config/bootstrap.conf + +add-peer + label ${ADAPTER_NAME}${THIS_LEG} + remote-name ${ADAPTER_NAME}${THIS_LEG} + remote-type active + heartbeat-time 15 + heartbeat-slack-time 5 +end-peer + +if "${FAILOVER}" == "ENABLED" + add-peer + label ${ADAPTER_NAME}${OTHER_LEG} + remote-type active + remote-name ${ADAPTER_NAME}${OTHER_LEG} + heartbeat-time 15 + heartbeat-slack-time 5 + end-peer +endif + +# Map the username into the container subject +object-map /PRIVATE/TODO/CONTAINER /PRIVATE/%u/TODO/CONTAINER + +add-data-service + service-name ${ADAPTER_NAME}TransformerContainerSvc${THIS_LEG} + include-pattern ^/PRIVATE/[0-9a-zA-Z@.-]/TODO + + add-source-group + required + if "${FAILOVER}" == "ENABLED" + if "${LOAD_BALANCING}" == "ENABLED" + # + # Load balancing with 2 legs + # + add-priority + label transformer${THIS_LEG} + label transformer${OTHER_LEG} + end-priority + else + # + # Failover with 2 legs + # + add-priority + label transformer${THIS_LEG} + end-priority + add-priority + label transformer${OTHER_LEG} + end-priority + endif + else + # + # One leg only + # + add-priority + label transformer${THIS_LEG} + end-priority + endif + end-source-group +end-data-service diff --git a/todo-template/blade/Transformer/etc/transformer.conf b/todo-template/blade/Transformer/etc/transformer.conf new file mode 100644 index 0000000..1ef9f0c --- /dev/null +++ b/todo-template/blade/Transformer/etc/transformer.conf @@ -0,0 +1,56 @@ +include-file ${ccd}/../../blade_config/bootstrap.conf + +add-peer + label ${ADAPTER_NAME}${THIS_LEG} + remote-name ${ADAPTER_NAME}${THIS_LEG} + remote-type active + heartbeat-time 15 + heartbeat-slack-time 5 +end-peer + +if "${FAILOVER}" == "ENABLED" + add-peer + label ${ADAPTER_NAME}${OTHER_LEG} + remote-type active + remote-name ${ADAPTER_NAME}${OTHER_LEG} + heartbeat-time 15 + heartbeat-slack-time 5 + end-peer +endif + +add-data-service + service-name ${ADAPTER_NAME}ContainerSvc${THIS_LEG} + include-pattern ^/PRIVATE/[0-9a-zA-Z@.-]/TODO + + add-source-group + required + if "${FAILOVER}" == "ENABLED" + if "${LOAD_BALANCING}" == "ENABLED" + # + # Load balancing with 2 legs + # + add-priority + label ${ADAPTER_NAME}${THIS_LEG} + label ${ADAPTER_NAME}${OTHER_LEG} + end-priority + else + # + # Failover with 2 legs + # + add-priority + label ${ADAPTER_NAME}${THIS_LEG} + end-priority + add-priority + label ${ADAPTER_NAME}${OTHER_LEG} + end-priority + endif + else + # + # One leg only + # + add-priority + label ${ADAPTER_NAME}${THIS_LEG} + end-priority + endif + end-source-group +end-data-service diff --git a/todo-template/blade/blade_config/bootstrap.conf b/todo-template/blade/blade_config/bootstrap.conf new file mode 100644 index 0000000..5aee86d --- /dev/null +++ b/todo-template/blade/blade_config/bootstrap.conf @@ -0,0 +1,5 @@ +define ADAPTER_NAME @adapterName@ + +if "${ENV:CONFIG_BASE}x" == "x" + include-file ${ccd}/environment-ide.conf +endif diff --git a/todo-template/blade/blade_config/fields.conf b/todo-template/blade/blade_config/fields.conf new file mode 100644 index 0000000..c3e5697 --- /dev/null +++ b/todo-template/blade/blade_config/fields.conf @@ -0,0 +1,4 @@ +add-field TYPE2_KEY -2500 + +add-field operation -3001 +add-field description -3002 \ No newline at end of file diff --git a/todo-template/build.gradle b/todo-template/build.gradle new file mode 100644 index 0000000..05b775b --- /dev/null +++ b/todo-template/build.gradle @@ -0,0 +1,96 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +apply plugin: 'java' + +ext { + major = 0 + minor = 0 + patch = 0 + + adapterName = project.name + replacements = [ + adapterName: adapterName, + ] +} + +version = major + "." + minor + "." + patch + + +repositories { + mavenCentral() +} + +dependencies { + compile(group: 'com.caplin.platform.components.datasource', name: 'datasource-java', version: '6.2.+') { transitive = false } +} + +jar { + manifest { + attributes( + 'Main-Class': 'com.caplin.template.TodoTemplateAdapter', + "Class-Path": configurations.compile.collect { it.getName() }.join(' ') + ) + } +} + + +task createKit(type: Zip, dependsOn: jar) { + archiveName = adapterName + "-" + version + ".zip" + def topDirName = archiveName.substring(0, archiveName.lastIndexOf(".")) + + into("${topDirName}") { + from "${project.projectDir}/blade" + include 'blade_config/bootstrap.conf' + if (!project.hasProperty("configOnly")) { + include 'DataSource/bin/start-jar.sh' + } + filter(ReplaceTokens, tokens: replacements) + } + + into("${topDirName}") { + from "${project.projectDir}/blade" + exclude 'blade_config/bootstrap.conf' + exclude 'DataSource/bin/start-jar.sh' + } + + if (!project.hasProperty("configOnly")) { + into("${topDirName}/DataSource/lib") { + from([project.jar.outputs, configurations.compile]) + } + } +} + +task setupWorkingDirectory(type: Copy) { + + from("${project.projectDir}/blade") { + include 'blade_config/bootstrap.conf' + filter(ReplaceTokens, tokens: replacements) + } + from("${project.projectDir}/blade") { + exclude 'blade_config/bootstrap.conf' + exclude 'DataSource/bin' + exclude 'Liberator' + exclude 'Transformer' + } + into("${buildDir}/env") +} + +task createEnvironmentConfig << { + String thisLeg = (project.hasProperty("thisLeg") ? project.thisLeg : "1") + + String content = """ + define THIS_LEG ${thisLeg} + define LIBERATOR${thisLeg}_HOST ${(project.hasProperty("liberatorHost") ? project.liberatorHost : "localhost")} + define LIBERATOR${thisLeg}_DATASRCPORT ${(project.hasProperty("liberatorDsPort") ? project.liberatorDsPort : "15001")} + define TRANSFORMER${thisLeg}_HOST ${(project.hasProperty("transformerHost") ? project.transformerHost : "localhost")} + define TRANSFORMER${thisLeg}_DATASRCPORT ${(project.hasProperty("transformerDsPort") ? project.transformerDsPort : "15002")} + """.stripIndent() + + + new File("${setupWorkingDirectory.getDestinationDir().getAbsolutePath()}/blade_config/").mkdirs() + new File("${setupWorkingDirectory.getDestinationDir().getAbsolutePath()}/blade_config/environment-ide.conf").text=content +} + +setupWorkingDirectory.finalizedBy createEnvironmentConfig + +assemble.dependsOn(createKit) diff --git a/todo-template/gradle/wrapper/gradle-wrapper.jar b/todo-template/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9411448 Binary files /dev/null and b/todo-template/gradle/wrapper/gradle-wrapper.jar differ diff --git a/todo-template/gradle/wrapper/gradle-wrapper.properties b/todo-template/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f7354aa --- /dev/null +++ b/todo-template/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Nov 24 12:19:48 GMT 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-bin.zip diff --git a/todo-template/gradlew b/todo-template/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/todo-template/gradlew @@ -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 "$@" diff --git a/todo-template/gradlew.bat b/todo-template/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/todo-template/gradlew.bat @@ -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 diff --git a/todo-template/libs/.gitkeep b/todo-template/libs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/todo-template/settings.gradle b/todo-template/settings.gradle new file mode 100644 index 0000000..6a834eb --- /dev/null +++ b/todo-template/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "TodoTemplateAdapter" diff --git a/todo-template/src/main/java/com/caplin/template/TodoContainerDataProvider.java b/todo-template/src/main/java/com/caplin/template/TodoContainerDataProvider.java new file mode 100644 index 0000000..3d6863b --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/TodoContainerDataProvider.java @@ -0,0 +1,139 @@ +package com.caplin.template; + +import java.util.*; +import java.util.regex.Pattern; + +import com.caplin.datasource.DataSource; +import com.caplin.datasource.messaging.container.ContainerMessage; +import com.caplin.datasource.messaging.record.GenericMessage; +import com.caplin.datasource.namespace.RegexNamespace; +import com.caplin.datasource.publisher.ActivePublisher; +import com.caplin.datasource.publisher.DataProvider; +import com.caplin.datasource.publisher.DiscardEvent; +import com.caplin.datasource.publisher.RequestEvent; +import com.caplin.template.service.TodoItem; +import com.caplin.template.service.TodoService; +import com.caplin.template.service.TodoSubscription; +import com.caplin.template.util.Observable; +import com.caplin.template.util.ObservableSubscription; + +class TodoContainerDataProvider { + + // Matches a typical username mapped in via Liberator + private static final String VALID_USERNAME_REGEX = "([0-9a-zA-Z@.-])"; + private static final String ITEM_UUID_REGEX = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"; + + private static final String CONTAINER_SUBJECT_REGEX = "^/PRIVATE/" + VALID_USERNAME_REGEX + "/TODO/CONTAINER$"; + private static final Pattern CONTAINER_SUBJECT_PATTERN = Pattern.compile(CONTAINER_SUBJECT_REGEX); + + private static final String CONTAINER_ROW_SUBJECT_REGEX = "^/PRIVATE/" + VALID_USERNAME_REGEX + "/TODO/ITEM/" + ITEM_UUID_REGEX + "$"; + private static final Pattern CONTAINER_ROW_SUBJECT_PATTERN = Pattern.compile(CONTAINER_ROW_SUBJECT_REGEX); + + private final DataSource dataSource; + private final TodoService todoService = new TodoService(); + private final Map> uuidToTodoItemObservable = new HashMap<>(); + + private ActivePublisher containerPublisher; + private ActivePublisher rowPublisher; + + TodoContainerDataProvider(final DataSource dataSource) { + this.dataSource = dataSource; + } + + void initialise() { + containerPublisher = dataSource.createActivePublisher(new RegexNamespace(CONTAINER_SUBJECT_REGEX), new DataProvider() { + + private final Map userToTodoSubscription = new HashMap<>(); + + @Override + public void onRequest(final RequestEvent requestEvent) { + final String username = CONTAINER_SUBJECT_PATTERN.matcher(requestEvent.getSubject()).group(1); + + final Set uuids = new LinkedHashSet<>(); + synchronized (uuids) { + // Publish an empty container immediately to let Liberator know we're accepting the request. + final ContainerMessage initialMessage = containerPublisher.getMessageFactory().createContainerMessage(requestEvent.getSubject()); + initialMessage.setDoNotAuthenticate(true); + uuids.forEach(uuid -> initialMessage.addElement("/PRIVATE/" + username + "/TODO/ITEM/" + uuid)); + initialMessage.setImage(true); + containerPublisher.publishInitialMessage(initialMessage); + } + + userToTodoSubscription.computeIfAbsent(username, usernameKey -> + todoService.subscribeForItems(usernameKey, todoItem -> + { + // Handle insert or update + final UUID uuid = todoItem.getId(); + final Observable prevObservableItem = uuidToTodoItemObservable.get(uuid); + final Observable observableItem = prevObservableItem == null ? new Observable<>() : prevObservableItem; + observableItem.onNext(todoItem); + + if (prevObservableItem == null) { + synchronized (uuids) { + uuids.add(uuid); + final ContainerMessage insertMessage = containerPublisher.getMessageFactory().createContainerMessage(requestEvent.getSubject()); + insertMessage.addElement("/PRIVATE/" + usernameKey + "/TODO/ITEM/" + uuid); + containerPublisher.publishToSubscribedPeers(insertMessage); + } + } + }, uuid -> + { + // Handle remove + uuidToTodoItemObservable.remove(uuid); + synchronized (uuids) { + uuids.remove(uuid); + final ContainerMessage removeMessage = containerPublisher.getMessageFactory().createContainerMessage(requestEvent.getSubject()); + removeMessage.removeElement("/PRIVATE/" + usernameKey + "/TODO/ITEM/" + uuid); + containerPublisher.publishToSubscribedPeers(removeMessage); + } + })); + } + + @Override + public void onDiscard(final DiscardEvent discardEvent) { + // Handle discard + final String username = CONTAINER_SUBJECT_PATTERN.matcher(discardEvent.getSubject()).group(1); + userToTodoSubscription.get(username).unsubscribe(); + } + }); + + rowPublisher = dataSource.createActivePublisher(new RegexNamespace(CONTAINER_ROW_SUBJECT_REGEX), new DataProvider() { + + private final Map subjectToTodoItemObservableSubscription = new HashMap<>(); + + @Override + public void onRequest(final RequestEvent requestEvent) { + final String subject = requestEvent.getSubject(); + final UUID uuid = UUID.fromString(CONTAINER_ROW_SUBJECT_PATTERN.matcher(subject).group(2)); + final Observable observableItem = uuidToTodoItemObservable.get(uuid); + + synchronized (observableItem) { + // Publish an initial image to the new peer, with the current value + final GenericMessage initialMessage = rowPublisher.getMessageFactory().createGenericMessage(subject); + final TodoItem latestValue = observableItem.getLatestValue(); + initialMessage.setField("value", latestValue.getValue()); + initialMessage.setField("priority", String.valueOf(latestValue.getPriority())); + initialMessage.setImage(true); + rowPublisher.publishInitialMessage(initialMessage); + } + + subjectToTodoItemObservableSubscription.computeIfAbsent(subject, itemUuid -> + observableItem.subscribe(todoItem -> { + synchronized (observableItem) { + // Publish any updates to all subscribed peers + final GenericMessage updateMessage = rowPublisher.getMessageFactory().createGenericMessage(subject); + updateMessage.setField("value", todoItem.getValue()); + updateMessage.setField("priority", String.valueOf(todoItem.getPriority())); + rowPublisher.publishToSubscribedPeers(updateMessage); + } + })); + } + + @Override + public void onDiscard(final DiscardEvent discardEvent) { + // Handle last peer discard, remove and cancel the future + subjectToTodoItemObservableSubscription.remove(discardEvent.getSubject()).unsubscribe(); + } + }); + } +} diff --git a/todo-template/src/main/java/com/caplin/template/TodoTemplateAdapter.java b/todo-template/src/main/java/com/caplin/template/TodoTemplateAdapter.java new file mode 100644 index 0000000..a804394 --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/TodoTemplateAdapter.java @@ -0,0 +1,28 @@ +package com.caplin.template; + +import com.caplin.datasource.DataSource; +import com.caplin.datasource.DataSourceFactory; + +public class TodoTemplateAdapter { + + private final DataSource dataSource; + + public TodoTemplateAdapter(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public void initialise() { + final TodoContainerDataProvider todoContainerDataProvider = new TodoContainerDataProvider(dataSource); + + todoContainerDataProvider.initialise(); + } + + public static void main(final String[] args) { + final DataSource dataSource = DataSourceFactory.createDataSource(args); + + final TodoTemplateAdapter todoTemplateAdapter = new TodoTemplateAdapter(dataSource); + todoTemplateAdapter.initialise(); + + dataSource.start(); + } +} diff --git a/todo-template/src/main/java/com/caplin/template/service/TodoItem.java b/todo-template/src/main/java/com/caplin/template/service/TodoItem.java new file mode 100644 index 0000000..e710d80 --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/service/TodoItem.java @@ -0,0 +1,59 @@ +package com.caplin.template.service; + +import java.util.Objects; +import java.util.UUID; + +public class TodoItem { + + private final UUID id = UUID.randomUUID(); + private String value; + private long priority; + + TodoItem(final String value, final long priority) { + this.value = value; + this.priority = priority; + } + + public UUID getId() { + return id; + } + + public String getValue() { + return value; + } + + public void setValue(final String value) { + this.value = value; + } + + public long getPriority() { + return priority; + } + + public void setPriority(final long priority) { + this.priority = priority; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TodoItem todoItem = (TodoItem) o; + return priority == todoItem.priority && + Objects.equals(id, todoItem.id) && + Objects.equals(value, todoItem.value); + } + + @Override + public int hashCode() { + return Objects.hash(id, value, priority); + } + + @Override + public String toString() { + return "TodoItem{" + "id=" + id + + ", value='" + value + '\'' + + ", priority=" + priority + + '}'; + } +} diff --git a/todo-template/src/main/java/com/caplin/template/service/TodoItemDeleteListener.java b/todo-template/src/main/java/com/caplin/template/service/TodoItemDeleteListener.java new file mode 100644 index 0000000..143ca07 --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/service/TodoItemDeleteListener.java @@ -0,0 +1,6 @@ +package com.caplin.template.service; + +import java.util.UUID; +import java.util.function.Consumer; + +public interface TodoItemDeleteListener extends Consumer {} diff --git a/todo-template/src/main/java/com/caplin/template/service/TodoItemInsertUpdateListener.java b/todo-template/src/main/java/com/caplin/template/service/TodoItemInsertUpdateListener.java new file mode 100644 index 0000000..68803ee --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/service/TodoItemInsertUpdateListener.java @@ -0,0 +1,5 @@ +package com.caplin.template.service; + +import java.util.function.Consumer; + +public interface TodoItemInsertUpdateListener extends Consumer {} diff --git a/todo-template/src/main/java/com/caplin/template/service/TodoService.java b/todo-template/src/main/java/com/caplin/template/service/TodoService.java new file mode 100644 index 0000000..4c78bcb --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/service/TodoService.java @@ -0,0 +1,63 @@ +package com.caplin.template.service; + +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class TodoService { + + private static final List TODO_VALUES = Arrays.asList( + "Walk the dog", + "Pick up milk", + "Dentist appointment", + "Clean the car", + "Mow the lawn", + "Water plants", + "Renew oyster card", + "Call dad"); + + private final ScheduledExecutorService scheduledExecutorService = newSingleThreadScheduledExecutor(); + private final Random random = new Random(); + + public TodoSubscription subscribeForItems(@SuppressWarnings("unused") final String username, final TodoItemInsertUpdateListener insertUpdateListener, + final TodoItemDeleteListener deleteListener) { + + final List items = new LinkedList<>(); + + final ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { + + if (items.size() < 4) { + final TodoItem todoItem = createTodoItem(); + items.add(todoItem); + insertUpdateListener.accept(todoItem); + } + else if (items.size() > 10) { + final TodoItem todoItem = items.remove(random.nextInt(items.size())); + deleteListener.accept(todoItem.getId()); + } + else { + if (random.nextInt(1) == 0) { + final TodoItem todoItem = createTodoItem(); + items.add(todoItem); + insertUpdateListener.accept(todoItem); + } else { + final TodoItem todoItem = items.remove(random.nextInt(items.size())); + deleteListener.accept(todoItem.getId()); + } + } + + }, 5, 5, TimeUnit.SECONDS); + + return () -> scheduledFuture.cancel(false); + } + + private TodoItem createTodoItem() { + return new TodoItem(TODO_VALUES.get(random.nextInt(TODO_VALUES.size())), random.nextInt(5)); + } +} diff --git a/todo-template/src/main/java/com/caplin/template/service/TodoSubscription.java b/todo-template/src/main/java/com/caplin/template/service/TodoSubscription.java new file mode 100644 index 0000000..c247491 --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/service/TodoSubscription.java @@ -0,0 +1,6 @@ +package com.caplin.template.service; + +public interface TodoSubscription { + + void unsubscribe(); +} diff --git a/todo-template/src/main/java/com/caplin/template/util/Observable.java b/todo-template/src/main/java/com/caplin/template/util/Observable.java new file mode 100644 index 0000000..9709433 --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/util/Observable.java @@ -0,0 +1,23 @@ +package com.caplin.template.util; + +public class Observable { + private Observer observer; + private T latestValue; + + public T getLatestValue() { + return latestValue; + } + + public void onNext(final T latestValue) { + this.latestValue = latestValue; + final Observer observer = this.observer; + if (observer != null) { + observer.accept(latestValue); + } + } + + public ObservableSubscription subscribe(final Observer observer) { + this.observer = observer; + return () -> this.observer = null; + } +} diff --git a/todo-template/src/main/java/com/caplin/template/util/ObservableSubscription.java b/todo-template/src/main/java/com/caplin/template/util/ObservableSubscription.java new file mode 100644 index 0000000..b10fa72 --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/util/ObservableSubscription.java @@ -0,0 +1,5 @@ +package com.caplin.template.util; + +public interface ObservableSubscription { + void unsubscribe(); +} diff --git a/todo-template/src/main/java/com/caplin/template/util/Observer.java b/todo-template/src/main/java/com/caplin/template/util/Observer.java new file mode 100644 index 0000000..ce1017a --- /dev/null +++ b/todo-template/src/main/java/com/caplin/template/util/Observer.java @@ -0,0 +1,5 @@ +package com.caplin.template.util; + +import java.util.function.Consumer; + +public interface Observer extends Consumer {}