From 43f02a6f19595a11e160d182615471e28791d785 Mon Sep 17 00:00:00 2001 From: Ross Anderson Date: Tue, 29 Nov 2016 15:19:04 +0000 Subject: [PATCH 1/6] Separate Container/Row/Type2 publishers Send container subscriptions via Transformer --- .../blade/DataSource/etc/datasource.conf | 24 +++++++ .../blade/Liberator/etc/rttpd.conf | 12 ++-- .../blade/Transformer/etc/transformer.conf | 57 ++++++++++++++++ pricing-template/build.gradle | 3 + .../template/PricingAdapterTemplate.java | 3 + .../ContainerTemplateDataProvider.java | 67 ++++++++----------- .../template/type2/Type2DataProvider.java | 47 +++++++++++++ 7 files changed, 168 insertions(+), 45 deletions(-) create mode 100644 pricing-template/blade/Transformer/etc/transformer.conf create mode 100644 pricing-template/src/main/java/com/caplin/template/type2/Type2DataProvider.java diff --git a/pricing-template/blade/DataSource/etc/datasource.conf b/pricing-template/blade/DataSource/etc/datasource.conf index f8e83f0..6dc2dfc 100644 --- a/pricing-template/blade/DataSource/etc/datasource.conf +++ b/pricing-template/blade/DataSource/etc/datasource.conf @@ -52,3 +52,27 @@ if "${FAILOVER}" == "ENABLED" 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/pricing-template/blade/Liberator/etc/rttpd.conf b/pricing-template/blade/Liberator/etc/rttpd.conf index c1e5643..daef8a9 100644 --- a/pricing-template/blade/Liberator/etc/rttpd.conf +++ b/pricing-template/blade/Liberator/etc/rttpd.conf @@ -93,7 +93,7 @@ add-data-service end-data-service add-data-service - service-name ${ADAPTER_NAME}ContainerSvc${THIS_LEG} + service-name ${ADAPTER_NAME}TransformerContainerSvc${THIS_LEG} include-pattern ^/TEMPLATE/CONTAINER include-pattern ^/TEMPLATE/TYPE2 @@ -105,18 +105,18 @@ add-data-service # Load balancing with 2 legs # add-priority - label ${ADAPTER_NAME}${THIS_LEG} - label ${ADAPTER_NAME}${OTHER_LEG} + label transformer${THIS_LEG} + label transformer${OTHER_LEG} end-priority else # # Failover with 2 legs # add-priority - label ${ADAPTER_NAME}${THIS_LEG} + label transformer${THIS_LEG} end-priority add-priority - label ${ADAPTER_NAME}${OTHER_LEG} + label transformer${OTHER_LEG} end-priority endif else @@ -124,7 +124,7 @@ add-data-service # One leg only # add-priority - label ${ADAPTER_NAME}${THIS_LEG} + label transformer${THIS_LEG} end-priority endif end-source-group diff --git a/pricing-template/blade/Transformer/etc/transformer.conf b/pricing-template/blade/Transformer/etc/transformer.conf new file mode 100644 index 0000000..2c6fefc --- /dev/null +++ b/pricing-template/blade/Transformer/etc/transformer.conf @@ -0,0 +1,57 @@ +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 ^/TEMPLATE/CONTAINER + include-pattern ^/TEMPLATE/TYPE2 + + 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/pricing-template/build.gradle b/pricing-template/build.gradle index dfe1deb..9d8a12f 100644 --- a/pricing-template/build.gradle +++ b/pricing-template/build.gradle @@ -72,6 +72,7 @@ task setupWorkingDirectory(type: Copy) { exclude 'blade_config/bootstrap.conf' exclude 'DataSource/bin' exclude 'Liberator' + exclude 'Transformer' } into("${buildDir}/env") } @@ -83,6 +84,8 @@ task createEnvironmentConfig << { 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() diff --git a/pricing-template/src/main/java/com/caplin/template/PricingAdapterTemplate.java b/pricing-template/src/main/java/com/caplin/template/PricingAdapterTemplate.java index dc4e602..0a761ba 100644 --- a/pricing-template/src/main/java/com/caplin/template/PricingAdapterTemplate.java +++ b/pricing-template/src/main/java/com/caplin/template/PricingAdapterTemplate.java @@ -5,6 +5,7 @@ import com.caplin.template.channel.ChannelTemplate; import com.caplin.template.container.ContainerTemplateDataProvider; import com.caplin.template.pricing.PricingTemplateDataProvider; +import com.caplin.template.type2.Type2DataProvider; public class PricingAdapterTemplate { @@ -18,10 +19,12 @@ public void initialise() { PricingTemplateDataProvider pricingTemplateDataProvider = new PricingTemplateDataProvider(dataSource); ContainerTemplateDataProvider containerTemplateDataProvider = new ContainerTemplateDataProvider(dataSource); ChannelTemplate channelTemplate = new ChannelTemplate(dataSource); + Type2DataProvider type2DataProvider = new Type2DataProvider(dataSource); pricingTemplateDataProvider.initialise(); containerTemplateDataProvider.initialise(); channelTemplate.initialise(); + type2DataProvider.initialise(); } public static void main(String[] args) { diff --git a/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java b/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java index 7bb246c..f1e79e2 100644 --- a/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java +++ b/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java @@ -1,68 +1,57 @@ package com.caplin.template.container; -import java.util.logging.Level; - import com.caplin.datasource.DataSource; import com.caplin.datasource.messaging.container.ContainerMessage; -import com.caplin.datasource.messaging.record.RecordType1Message; -import com.caplin.datasource.messaging.record.RecordType2Message; +import com.caplin.datasource.messaging.record.GenericMessage; import com.caplin.datasource.namespace.PrefixNamespace; import com.caplin.datasource.publisher.ActivePublisher; import com.caplin.datasource.publisher.DataProvider; import com.caplin.datasource.publisher.DiscardEvent; import com.caplin.datasource.publisher.RequestEvent; -public class ContainerTemplateDataProvider implements DataProvider { +public class ContainerTemplateDataProvider { private static final int NUM_CONSTITUENTS = 50; private final DataSource dataSource; - private ActivePublisher publisher; + private ActivePublisher containerPublisher; + private ActivePublisher rowPublisher; public ContainerTemplateDataProvider(DataSource dataSource) { this.dataSource = dataSource; } public void initialise() { - publisher = dataSource.createActivePublisher(new PrefixNamespace("/TEMPLATE/CONTAINER"), this); - } + containerPublisher = dataSource.createActivePublisher(new PrefixNamespace("/TEMPLATE/CONTAINER/ALL"), new DataProvider() { + @Override + public void onRequest(RequestEvent requestEvent) { + ContainerMessage containerMessage = containerPublisher.getMessageFactory().createContainerMessage(requestEvent.getSubject()); + containerMessage.setDoNotAuthenticate(true); - @Override - public void onRequest(RequestEvent requestEvent) { - if (requestEvent.getSubject().equals("/TEMPLATE/CONTAINER/ALL")) { - ContainerMessage containerMessage = publisher.getMessageFactory().createContainerMessage(requestEvent.getSubject()); - containerMessage.setDoNotAuthenticate(true); + for (int i = 0; i < NUM_CONSTITUENTS; i++) { + containerMessage.addElement("/TEMPLATE/CONTAINERROW_" + i); + } - for (int i = 0; i < NUM_CONSTITUENTS; i++) { - containerMessage.addElement("/TEMPLATE/CONTAINERROW_" + i); + containerMessage.setImage(true); + containerPublisher.publishInitialMessage(containerMessage); } - containerMessage.setImage(true); - publisher.publishInitialMessage(containerMessage); - } else if (requestEvent.getSubject().startsWith("/TEMPLATE/CONTAINERROW")) { - RecordType1Message message = publisher.getMessageFactory().createRecordType1Message(requestEvent.getSubject()); - - message.setImage(true); - publisher.publishInitialMessage(message); - } else if (requestEvent.getSubject().equals("/TEMPLATE/TYPE2/ALL")) { - for (int i = 0; i < NUM_CONSTITUENTS; i++) { - - RecordType2Message type2Message = publisher.getMessageFactory().createRecordType2Message(requestEvent.getSubject(), "TYPE2_KEY", String.valueOf(i)); - - if (i == 0) { - type2Message.setImage(true); - publisher.publishInitialMessage(type2Message); - } else { - publisher.publishToSubscribedPeers(type2Message); - } + @Override + public void onDiscard(DiscardEvent discardEvent) {} + }); + + rowPublisher = dataSource.createActivePublisher(new PrefixNamespace("/TEMPLATE/CONTAINER/CONTAINERROW"), new DataProvider() { + @Override + public void onRequest(RequestEvent requestEvent) { + GenericMessage updateMessage = rowPublisher.getMessageFactory().createGenericMessage(requestEvent.getSubject()); + updateMessage.setField("currentTime", String.valueOf(System.currentTimeMillis())); + rowPublisher.publishInitialMessage(updateMessage); // Publish this update to any new peers + rowPublisher.publishToSubscribedPeers(updateMessage); // Publish this update to any existing peers } - } else { - dataSource.getLogger().log(Level.INFO, "Unknown subject " + requestEvent.getSubject()); - } + @Override + public void onDiscard(DiscardEvent discardEvent) {} + }); } - - @Override - public void onDiscard(DiscardEvent discardEvent) {} } diff --git a/pricing-template/src/main/java/com/caplin/template/type2/Type2DataProvider.java b/pricing-template/src/main/java/com/caplin/template/type2/Type2DataProvider.java new file mode 100644 index 0000000..815d88c --- /dev/null +++ b/pricing-template/src/main/java/com/caplin/template/type2/Type2DataProvider.java @@ -0,0 +1,47 @@ +package com.caplin.template.type2; + +import com.caplin.datasource.DataSource; +import com.caplin.datasource.messaging.record.RecordType2Message; +import com.caplin.datasource.namespace.PrefixNamespace; +import com.caplin.datasource.publisher.ActivePublisher; +import com.caplin.datasource.publisher.DataProvider; +import com.caplin.datasource.publisher.DiscardEvent; +import com.caplin.datasource.publisher.RequestEvent; + +public class Type2DataProvider { + + private static final int NUM_CONSTITUENTS = 50; + + private final DataSource dataSource; + + private ActivePublisher publisher; + + public Type2DataProvider(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void initialise() { + publisher = dataSource.createActivePublisher(new PrefixNamespace("/TEMPLATE/TYPE2/ALL"), new DataProvider() { + @Override + public void onRequest(RequestEvent requestEvent) { + for (int i = 0; i < NUM_CONSTITUENTS; i++) { + + RecordType2Message type2Message = publisher.getMessageFactory().createRecordType2Message( + requestEvent.getSubject(), "TYPE2_KEY", String.valueOf(i)); + + if (i == 0) { + type2Message.setImage(true); + publisher.publishInitialMessage(type2Message); + } else { + publisher.publishToSubscribedPeers(type2Message); + } + } + } + + @Override + public void onDiscard(DiscardEvent discardEvent) { + // Handle discard + } + }); + } +} From ff9c7fdad8d3a7ee5efd048bd27e0c136f54de11 Mon Sep 17 00:00:00 2001 From: Ross Anderson Date: Tue, 29 Nov 2016 15:20:09 +0000 Subject: [PATCH 2/6] Remove redundant call --- .../template/container/ContainerTemplateDataProvider.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java b/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java index f1e79e2..e6f6cc2 100644 --- a/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java +++ b/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java @@ -46,8 +46,7 @@ public void onDiscard(DiscardEvent discardEvent) {} public void onRequest(RequestEvent requestEvent) { GenericMessage updateMessage = rowPublisher.getMessageFactory().createGenericMessage(requestEvent.getSubject()); updateMessage.setField("currentTime", String.valueOf(System.currentTimeMillis())); - rowPublisher.publishInitialMessage(updateMessage); // Publish this update to any new peers - rowPublisher.publishToSubscribedPeers(updateMessage); // Publish this update to any existing peers + rowPublisher.publishInitialMessage(updateMessage); } @Override From 1c89ce726e7d7a3c68fe88b997871765ac2a9834 Mon Sep 17 00:00:00 2001 From: Ross Anderson Date: Wed, 25 Jan 2017 16:46:25 +0000 Subject: [PATCH 3/6] Feature/todo list adapter (#6) * Add a "Todo List" adapter * Use locks for simplified code. Resend container contents to new peers. * naming improvements --- build.gradle | 3 - settings.gradle | 3 +- todo-template/README.md | 46 +++++ .../blade/DataSource/bin/start-jar.sh | 33 ++++ .../blade/DataSource/etc/datasource.conf | 78 +++++++++ todo-template/blade/DataSource/var/.gitkeep | 0 todo-template/blade/Liberator/etc/rttpd.conf | 59 +++++++ .../blade/Transformer/etc/transformer.conf | 56 ++++++ .../blade/blade_config/bootstrap.conf | 5 + todo-template/blade/blade_config/fields.conf | 4 + todo-template/build.gradle | 96 +++++++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + todo-template/gradlew | 160 ++++++++++++++++++ todo-template/gradlew.bat | 90 ++++++++++ todo-template/libs/.gitkeep | 0 todo-template/settings.gradle | 1 + .../template/TodoContainerDataProvider.java | 139 +++++++++++++++ .../caplin/template/TodoTemplateAdapter.java | 28 +++ .../com/caplin/template/service/TodoItem.java | 59 +++++++ .../service/TodoItemDeleteListener.java | 6 + .../service/TodoItemInsertUpdateListener.java | 5 + .../caplin/template/service/TodoService.java | 63 +++++++ .../template/service/TodoSubscription.java | 6 + .../com/caplin/template/util/Observable.java | 23 +++ .../template/util/ObservableSubscription.java | 5 + .../com/caplin/template/util/Observer.java | 5 + 27 files changed, 975 insertions(+), 4 deletions(-) delete mode 100644 build.gradle create mode 100644 todo-template/README.md create mode 100644 todo-template/blade/DataSource/bin/start-jar.sh create mode 100644 todo-template/blade/DataSource/etc/datasource.conf create mode 100644 todo-template/blade/DataSource/var/.gitkeep create mode 100644 todo-template/blade/Liberator/etc/rttpd.conf create mode 100644 todo-template/blade/Transformer/etc/transformer.conf create mode 100644 todo-template/blade/blade_config/bootstrap.conf create mode 100644 todo-template/blade/blade_config/fields.conf create mode 100644 todo-template/build.gradle create mode 100644 todo-template/gradle/wrapper/gradle-wrapper.jar create mode 100644 todo-template/gradle/wrapper/gradle-wrapper.properties create mode 100755 todo-template/gradlew create mode 100644 todo-template/gradlew.bat create mode 100644 todo-template/libs/.gitkeep create mode 100644 todo-template/settings.gradle create mode 100644 todo-template/src/main/java/com/caplin/template/TodoContainerDataProvider.java create mode 100644 todo-template/src/main/java/com/caplin/template/TodoTemplateAdapter.java create mode 100644 todo-template/src/main/java/com/caplin/template/service/TodoItem.java create mode 100644 todo-template/src/main/java/com/caplin/template/service/TodoItemDeleteListener.java create mode 100644 todo-template/src/main/java/com/caplin/template/service/TodoItemInsertUpdateListener.java create mode 100644 todo-template/src/main/java/com/caplin/template/service/TodoService.java create mode 100644 todo-template/src/main/java/com/caplin/template/service/TodoSubscription.java create mode 100644 todo-template/src/main/java/com/caplin/template/util/Observable.java create mode 100644 todo-template/src/main/java/com/caplin/template/util/ObservableSubscription.java create mode 100644 todo-template/src/main/java/com/caplin/template/util/Observer.java diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 6587d1d..0000000 --- a/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -allprojects { - apply plugin: 'idea' -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fe808c8..0ef9381 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,5 @@ include ':blotter-template', ':notification-template', ':permissioning-template', ':pricing-template', - ':trading-template' \ No newline at end of file + ':trading-template', + ':todo-template' \ No newline at end of file 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 0000000000000000000000000000000000000000..941144813d241db74e1bf25b6804c679fbe7f0a3 GIT binary patch literal 53636 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 DXC5Z7 literal 0 HcmV?d00001 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 {} From a2dd8c45a78dcee3c4c638c002aad66f5ce71c7e Mon Sep 17 00:00:00 2001 From: Christoph Sitter Date: Mon, 12 Jun 2017 16:21:01 +0100 Subject: [PATCH 4/6] fix config for pricing template --- .../blade/DataSource/etc/datasource.conf | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/pricing-template/blade/DataSource/etc/datasource.conf b/pricing-template/blade/DataSource/etc/datasource.conf index 25c08e4..bdd6893 100644 --- a/pricing-template/blade/DataSource/etc/datasource.conf +++ b/pricing-template/blade/DataSource/etc/datasource.conf @@ -78,27 +78,3 @@ else end-peer endif 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 From 12b2ac8c6b4cad2e550171702a44f97036d8d90c Mon Sep 17 00:00:00 2001 From: Christoph Sitter Date: Mon, 12 Jun 2017 16:25:04 +0100 Subject: [PATCH 5/6] Incorporate improvements from @rossdanderson --- .../template/container/ContainerTemplateDataProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java b/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java index e6f6cc2..fb79f86 100644 --- a/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java +++ b/pricing-template/src/main/java/com/caplin/template/container/ContainerTemplateDataProvider.java @@ -41,7 +41,7 @@ public void onRequest(RequestEvent requestEvent) { public void onDiscard(DiscardEvent discardEvent) {} }); - rowPublisher = dataSource.createActivePublisher(new PrefixNamespace("/TEMPLATE/CONTAINER/CONTAINERROW"), new DataProvider() { + rowPublisher = dataSource.createActivePublisher(new PrefixNamespace("/TEMPLATE/CONTAINERROW_"), new DataProvider() { @Override public void onRequest(RequestEvent requestEvent) { GenericMessage updateMessage = rowPublisher.getMessageFactory().createGenericMessage(requestEvent.getSubject()); From 2cb8af035e4e8a23e81e33952129ff4d274ebaf5 Mon Sep 17 00:00:00 2001 From: Christoph Sitter Date: Mon, 12 Jun 2017 17:20:16 +0100 Subject: [PATCH 6/6] use nexus url and credentials from gradle.properties file --- blotter-template/build.gradle | 8 +++----- jtm-legacy-template/build.gradle | 6 +++--- jtm-template/build.gradle | 15 ++++++--------- notification-template/build.gradle | 10 ++++------ permissioning-template/build.gradle | 8 ++++---- pricing-template/build.gradle | 6 +++--- trading-template/build.gradle | 9 ++++----- 7 files changed, 27 insertions(+), 35 deletions(-) diff --git a/blotter-template/build.gradle b/blotter-template/build.gradle index 9a759d1..d6749a8 100644 --- a/blotter-template/build.gradle +++ b/blotter-template/build.gradle @@ -12,22 +12,20 @@ ext { replacements = [ adapterName: adapterName, ] - } version = major + "." + minor + "." + patch - repositories { flatDir { dirs 'lib' } maven { credentials { - username "user" //insert your caplin artifactory username here - password "secret" //insert your caplin artifactory password here + username "$caplinNexusUser" + password "$caplinNexusSecret" } - url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa-cache" + url "http://nexus.caplin.com:8081/caplin-release" } } diff --git a/jtm-legacy-template/build.gradle b/jtm-legacy-template/build.gradle index e145cec..9f17f6b 100644 --- a/jtm-legacy-template/build.gradle +++ b/jtm-legacy-template/build.gradle @@ -22,10 +22,10 @@ repositories { } maven { credentials { - username "user" //insert your caplin artifactory username here - password "secret" //insert your caplin artifactory password here + username "$caplinNexusUser" + password "$caplinNexusSecret" } - url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa-cache" + url "http://nexus.caplin.com:8081/caplin-release" } } diff --git a/jtm-template/build.gradle b/jtm-template/build.gradle index 1fd1132..04d72f4 100644 --- a/jtm-template/build.gradle +++ b/jtm-template/build.gradle @@ -21,19 +21,16 @@ repositories { dirs 'lib' } maven { - mavenCentral() -// credentials { -// username "user" //insert your caplin artifactory username here -// password "secret" //insert your caplin artifactory password here -// } -// url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa-cache" - url "http://artifactory.caplin.com/artifactory/caplin-ci" -// } + credentials { + username "$caplinNexusUser" + password "$caplinNexusSecret" + } + url "http://nexus.caplin.com:8081/caplin-release" } } dependencies { - compile(group: 'com.caplin.platform.components', name: 'JTM', version: '7.0.+') {transitive = false} + compile(group: 'com.caplin.platform.components', name: 'JTM', version: '7.0.+') { transitive = false } } jar { diff --git a/notification-template/build.gradle b/notification-template/build.gradle index 3b9cbdf..5ddbe7d 100644 --- a/notification-template/build.gradle +++ b/notification-template/build.gradle @@ -12,22 +12,20 @@ ext { replacements = [ adapterName: adapterName, ] - } version = major + "." + minor + "." + patch - repositories { flatDir { dirs 'lib' } - maven { + maven { credentials { - username "user" //insert your caplin artifactory username here - password "secret" //insert your caplin artifactory password here + username "$caplinNexusUser" + password "$caplinNexusSecret" } - url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa-cache" + url "http://nexus.caplin.com:8081/caplin-release" } } diff --git a/permissioning-template/build.gradle b/permissioning-template/build.gradle index 57eac96..88bb8cc 100644 --- a/permissioning-template/build.gradle +++ b/permissioning-template/build.gradle @@ -20,12 +20,12 @@ repositories { flatDir { dirs 'lib' } - maven { + maven { credentials { - username "user" //insert your caplin artifactory username here - password "secret" //insert your caplin artifactory password here + username "$caplinNexusUser" + password "$caplinNexusSecret" } - url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa-cache" + url "http://nexus.caplin.com:8081/caplin-release" } } diff --git a/pricing-template/build.gradle b/pricing-template/build.gradle index 6abbb8c..85d6d8a 100644 --- a/pricing-template/build.gradle +++ b/pricing-template/build.gradle @@ -22,10 +22,10 @@ repositories { } maven { credentials { - username "user" //insert your caplin artifactory username here - password "secret" //insert your caplin artifactory password here + username "$caplinNexusUser" + password "$caplinNexusSecret" } - url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa-cache" + url "http://nexus.caplin.com:8081/caplin-release" } } diff --git a/trading-template/build.gradle b/trading-template/build.gradle index 093ce6c..796ed34 100644 --- a/trading-template/build.gradle +++ b/trading-template/build.gradle @@ -20,19 +20,18 @@ repositories { flatDir { dirs 'lib' } - maven { + maven { credentials { - username "user" //insert your caplin artifactory username here - password "secret" //insert your caplin artifactory password here + username "$caplinNexusUser" + password "$caplinNexusSecret" } - url "http://clientartifactory.caplin.com/artifactory/caplin-artifactory-caplin-qa" + url "http://nexus.caplin.com:8081/caplin-release" } } dependencies { compile(group: 'com.caplin.platform.components.datasource', name: 'datasource-java', version: '6.2.+') { transitive = false } compile(group: 'com.caplin.platform.components.trading', name: 'trading-datasource', version: '6.2.+') { transitive = false } - } jar {