From 87a1dcd95770e74db5671d3e9131ad60e866b9c7 Mon Sep 17 00:00:00 2001
From: rschoene <rene.schoene@tu-dresden.de>
Date: Mon, 2 May 2022 11:06:34 +0200
Subject: [PATCH] Updated initial version of Java handler.

- also missing fix for CI (ragdoc-view should always run)
---
 .gitlab-ci.yml                                |  3 -
 .../src/main/jastadd/Handlers.jrag            |  1 +
 .../jastadd/ragconnect/compiler/Compiler.java |  3 +
 .../src/main/resources/JavaHandler.mustache   | 88 +++++++++++++++++++
 .../src/main/resources/handler.mustache       | 10 ++-
 .../main/resources/receiveDefinition.mustache | 12 +++
 .../main/resources/sendDefinition.mustache    | 22 +++++
 ragconnect.tests/build.gradle                 | 29 +++++-
 .../src/test/01-input/java/README.md          |  3 +
 .../src/test/01-input/java/Test.connect       | 25 ++++++
 .../src/test/01-input/java/Test.jadd          | 30 +++++++
 .../src/test/01-input/java/Test.relast        |  9 ++
 .../jastadd/ragconnect/tests/JavaTest.java    | 43 +++++++++
 13 files changed, 270 insertions(+), 8 deletions(-)
 create mode 100644 ragconnect.base/src/main/resources/JavaHandler.mustache
 create mode 100644 ragconnect.tests/src/test/01-input/java/README.md
 create mode 100644 ragconnect.tests/src/test/01-input/java/Test.connect
 create mode 100644 ragconnect.tests/src/test/01-input/java/Test.jadd
 create mode 100644 ragconnect.tests/src/test/01-input/java/Test.relast
 create mode 100644 ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 87d2c9c..44a279a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -94,9 +94,6 @@ ragdoc_view:
     - OUTPUT_DIR=$(pwd -P)/pages/docs/ragdoc
     - cd /ragdoc-view/src/ && rm -rf data && ln -s $DATA_DIR
     - /ragdoc-view/build-view.sh --output-path=$OUTPUT_DIR
-  only:
-    - dev
-    - master
   artifacts:
     paths:
       - "pages/docs/ragdoc"
diff --git a/ragconnect.base/src/main/jastadd/Handlers.jrag b/ragconnect.base/src/main/jastadd/Handlers.jrag
index 57c7ccd..e64a8c5 100644
--- a/ragconnect.base/src/main/jastadd/Handlers.jrag
+++ b/ragconnect.base/src/main/jastadd/Handlers.jrag
@@ -1,4 +1,5 @@
 aspect RagConnectHandlers {
+  syn Handler RagConnect.javaHandler() = resolveHandlerByName("java");
   syn Handler RagConnect.mqttHandler() = resolveHandlerByName("mqtt");
   syn Handler RagConnect.restHandler() = resolveHandlerByName("rest");
 
diff --git a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
index 8d4ab2c..e255b3b 100644
--- a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
+++ b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
@@ -35,6 +35,7 @@ public class Compiler extends AbstractCompiler {
   private static final String OPTION_LOGGING_TARGET_CONSOLE = "console";
   private static final String OPTION_LOGGING_TARGET_SLF4J = "slf4j";
 
+  private static final String OPTION_PROTOCOL_JAVA = "java";
   private static final String OPTION_PROTOCOL_MQTT = "mqtt";
   private static final String OPTION_PROTOCOL_REST = "rest";
 
@@ -179,6 +180,7 @@ public class Compiler extends AbstractCompiler {
         new ValueOption("protocols", "Protocols to enable")
             .acceptMultipleValues(true)
             .addDefaultValue(OPTION_PROTOCOL_MQTT, "Enable MQTT")
+            .addAcceptedValue(OPTION_PROTOCOL_JAVA, "Enable Java")
             .addAcceptedValue(OPTION_PROTOCOL_REST, "Enable REST")
     );
     optionPrintYaml = addOption(
@@ -331,6 +333,7 @@ public class Compiler extends AbstractCompiler {
     ragConnect.getConfiguration().setRootNode(rootNode);
 
     // Handler ::= <ClassName> <UniqueName> <InUse:boolean>;
+    ragConnect.addHandler(new Handler("JavaHandler", "java", optionProtocols.hasValue(OPTION_PROTOCOL_JAVA)));
     ragConnect.addHandler(new Handler("MqttServerHandler", "mqtt", optionProtocols.hasValue(OPTION_PROTOCOL_MQTT)));
     ragConnect.addHandler(new Handler("RestServerHandler", "rest", optionProtocols.hasValue(OPTION_PROTOCOL_REST)));
   }
diff --git a/ragconnect.base/src/main/resources/JavaHandler.mustache b/ragconnect.base/src/main/resources/JavaHandler.mustache
new file mode 100644
index 0000000..bc73c91
--- /dev/null
+++ b/ragconnect.base/src/main/resources/JavaHandler.mustache
@@ -0,0 +1,88 @@
+/**
+ * Singleton class providing routing functionality for byte[] based message calls.
+ */
+public class JavaHandler {
+  public static JavaHandler JAVA_HANDLER_INSTANCE = null;
+  private java.util.Map<String, java.util.List<Pair<String, java.util.function.BiConsumer<String, byte[]>>>> callbackList = new java.util.concurrent.ConcurrentHashMap<>();
+  private String name;
+
+  private JavaHandler() {
+    this("RagConnect");
+  }
+
+  public JavaHandler(String name) {
+    this.name = name;
+  }
+
+  public synchronized static JavaHandler getInstance() {
+    if (JAVA_HANDLER_INSTANCE == null) {
+      JAVA_HANDLER_INSTANCE = new JavaHandler();
+    }
+    return JAVA_HANDLER_INSTANCE;
+  }
+
+  public String registerCallback(String topic, java.util.function.BiConsumer<String, byte[]> callback) {
+    {{logInfo}}("[JAVA_HANDLER] Registering new callback for {{log_}}.", topic);
+
+    String callbackUUID = java.util.UUID.randomUUID().toString();
+
+    java.util.List<Pair<String, java.util.function.BiConsumer<String, byte[]>>> registeredCallbacks = getAllCallbacks().get(topic);
+
+    if (registeredCallbacks == null) {
+      java.util.List<Pair<String, java.util.function.BiConsumer<String, byte[]>>> newCallbackList = java.util.Collections.synchronizedList(new java.util.ArrayList<>());
+      newCallbackList.add(new Pair<>(callbackUUID, callback));
+      callbackList.put(topic, newCallbackList);
+    } else {
+      registeredCallbacks.add(new Pair<>(callbackUUID, callback));
+    }
+
+    return callbackUUID;
+  }
+
+  public boolean unregisterCallback(String path, String uuid) {
+    {{logInfo}}("[JAVA_HANDLER] Unregistering callback with uuid: {{log_}}", uuid + " on path: {{log_}}", path);
+
+    java.util.List<Pair<String, java.util.function.BiConsumer<String, byte[]>>> callbacks = getAllCallbacks().get(path);
+
+    int count = 0;
+
+    if (callbacks != null) {
+      for (Pair<String, java.util.function.BiConsumer<String, byte[]>> callbackPair : callbacks) {
+        if (callbackPair._1.equals(uuid)) {
+          callbacks.remove(count);
+          return true;
+        } else {
+          count++;
+        }
+      }
+    }
+    return false;
+  }
+
+  public void close() {
+  }
+
+  public boolean push(String topic, byte[] data) {
+    {{logDebug}}("[JAVA_HANDLER] Pushing a message.");
+    String dataString = new String(data);
+    {{logDebug}}("[JAVA_HANDLER] Data: {{log_}}", dataString);
+
+    java.util.List<Pair<String, java.util.function.BiConsumer<String, byte[]>>> callbacks = getAllCallbacks().get(topic);
+
+    if (callbacks == null) {
+      {{logError}}("[JAVA_HANDLER] Could not publish message. No callback registered for topic {{log_}}", topic);
+      return false;
+    }
+
+    for (Pair<String, java.util.function.BiConsumer<String, byte[]>> callbackPair : callbacks) {
+      {{logDebug}}("[JAVA_HANDLER] Calling callback: {{log_}}", callbackPair._1);
+      callbackPair._2.accept(topic, data);
+    }
+
+    return true;
+  }
+
+  public java.util.Map<String, java.util.List<Pair<String, java.util.function.BiConsumer<String, byte[]>>>> getAllCallbacks() {
+    return callbackList;
+  }
+}
diff --git a/ragconnect.base/src/main/resources/handler.mustache b/ragconnect.base/src/main/resources/handler.mustache
index 4919426..2c4cf96 100644
--- a/ragconnect.base/src/main/resources/handler.mustache
+++ b/ragconnect.base/src/main/resources/handler.mustache
@@ -19,18 +19,24 @@ aspect RagConnectHandler {
     {{/configIncrementalOptionActive}}
   }
 
+{{#javaHandler}}
+  {{#InUse}}
+  {{> JavaHandler}}
+  {{/InUse}}
+{{/javaHandler}}
+
 {{#mqttHandler}}
   {{#InUse}}
   public void {{rootNodeName}}.{{setupWaitUntilReadyMethodName}}(long time, java.util.concurrent.TimeUnit unit) {
     {{fieldName}}.setupWaitUntilReady(time, unit);
   }
+  {{> MqttHandler}}
   {{/InUse}}
-{{> MqttHandler}}
 {{/mqttHandler}}
 
 {{#restHandler}}
   {{#InUse}}
-{{> RestHandler}}
+  {{> RestHandler}}
   {{/InUse}}
 {{/restHandler}}
 
diff --git a/ragconnect.base/src/main/resources/receiveDefinition.mustache b/ragconnect.base/src/main/resources/receiveDefinition.mustache
index cb4dc8d..c4f070c 100644
--- a/ragconnect.base/src/main/resources/receiveDefinition.mustache
+++ b/ragconnect.base/src/main/resources/receiveDefinition.mustache
@@ -93,6 +93,13 @@ private boolean {{parentTypeName}}.{{internalConnectMethodName}}(String {{connec
   RagConnectToken connectToken = new RagConnectToken(uri, "{{entityName}}");
   boolean success;
   switch (scheme) {
+  {{#javaHandler}}
+  {{#InUse}}
+    case "java":
+      success = {{attributeName}}().registerCallback(path, consumer, connectToken);
+      break;
+  {{/InUse}}
+  {{/javaHandler}}
   {{#mqttHandler}}
   {{#InUse}}
     case "mqtt":
@@ -130,6 +137,11 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam
   }
   RagConnectDisconnectHandlerMethod disconnectingMethod;
   switch (scheme) {
+  {{#javaHandler}}
+  {{#InUse}}
+    case "java": return {{attributeName}}().unregisterCallback(uri.getPath(), connectTokens.get(this).get(uri).globalId);
+  {{/InUse}}
+  {{/javaHandler}}
   {{#mqttHandler}}
   {{#InUse}}
     case "mqtt": disconnectingMethod = {{attributeName}}()::disconnect;
diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache
index 1e66fde..1485c17 100644
--- a/ragconnect.base/src/main/resources/sendDefinition.mustache
+++ b/ragconnect.base/src/main/resources/sendDefinition.mustache
@@ -5,6 +5,21 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
   RagConnectToken connectToken = new RagConnectToken(uri, "{{entityName}}");
   boolean success;
   switch (scheme) {
+  {{#javaHandler}}
+  {{#InUse}}
+    case "java":
+    final JavaHandler handler = {{attributeName}}().getInstance();
+
+    {{senderName}}.add(() -> {
+      handler.push(path, {{lastValueGetterCall}});
+    }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken);
+    {{updateMethodName}}();
+    if (writeCurrentValue) {
+      {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken);
+    }
+    break;
+  {{/InUse}}
+  {{/javaHandler}}
   {{#mqttHandler}}
   {{#InUse}}
     case "mqtt":
@@ -78,6 +93,13 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam
   {{/configIncrementalOptionActive}}
   RagConnectDisconnectHandlerMethod disconnectingMethod;
   switch (scheme) {
+  {{#javaHandler}}
+  {{#InUse}}
+    case "java":
+      disconnectingMethod = {{senderName}}::remove;
+    break;
+  {{/InUse}}
+  {{/javaHandler}}
   {{#mqttHandler}}
   {{#InUse}}
     case "mqtt":
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index 18dd839..d41a73c 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -649,8 +649,6 @@ task compileRelationIncremental(type: RagConnectTest) {
         inputFiles = [file('src/test/01-input/relation/Test.relast'),
                       file('src/test/01-input/relation/Test.connect')]
         rootNode = 'Root'
-        logWrites = true
-        logIncremental = true
         extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329'])
     }
     relast {
@@ -666,9 +664,34 @@ task compileRelationIncremental(type: RagConnectTest) {
     }
 }
 
+// --- Test: java-incremental ---
+task compileJavaIncremental(type: RagConnectTest) {
+    ragconnect {
+        outputDir = file('src/test/02-after-ragconnect/javaInc')
+        inputFiles = [file('src/test/01-input/java/Test.relast'),
+                      file('src/test/01-input/java/Test.connect')]
+        rootNode = 'Root'
+        logWrites = true
+        logIncremental = true
+        protocols = ['java']
+        extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329'])
+    }
+    relast {
+        useJastAddNames = true
+        grammarName = 'src/test/03-after-relast/javaInc/javaInc'
+        serializer = 'jackson'
+    }
+    jastadd {
+        jastAddList = 'JastAddList'
+        packageName = 'javaInc.ast'
+        inputFiles = [file('src/test/01-input/java/Test.jadd')]
+        extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL
+    }
+}
+
 // --- Task order ---
 classes.dependsOn(':ragconnect.base:jar')
-//compileAttributeIncremental.outputs.upToDateWhen { false }
+compileJavaIncremental.outputs.upToDateWhen { false }
 
 // --- Misc ---
 static ArrayList<String> defaultRagConnectOptionsAnd(ArrayList<String> options = []) {
diff --git a/ragconnect.tests/src/test/01-input/java/README.md b/ragconnect.tests/src/test/01-input/java/README.md
new file mode 100644
index 0000000..272a851
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/java/README.md
@@ -0,0 +1,3 @@
+# Java
+
+Idea: Use receive and send definitions using the Java handler.
diff --git a/ragconnect.tests/src/test/01-input/java/Test.connect b/ragconnect.tests/src/test/01-input/java/Test.connect
new file mode 100644
index 0000000..dcb6a56
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/java/Test.connect
@@ -0,0 +1,25 @@
+send SenderRoot.SendToken ;
+send SenderRoot.SendNode ;
+send SenderRoot.SendManyNode ;
+send SenderRoot.SendNTA ;
+
+AddSuffix maps A a to A {:
+  A result = new A();
+  String changedValue = a.getValue() + "post";
+  result.setValue(changedValue);
+  result.setInner(new Inner("inner" + a.getInner().getInnerValue()));
+  return result;
+:}
+
+AddStringSuffix maps String s to String {:
+  return s + "post";
+:}
+
+AddPlusOne maps int i to int {:
+  return i + 1;
+:}
+
+receive ReceiverRoot.SomeToken;
+receive ReceiverRoot.SomeNode;
+receive ReceiverRoot.SomeNodeWithMapping using AddSuffix;
+receive ReceiverRoot.ManyNode;
diff --git a/ragconnect.tests/src/test/01-input/java/Test.jadd b/ragconnect.tests/src/test/01-input/java/Test.jadd
new file mode 100644
index 0000000..0bb643e
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/java/Test.jadd
@@ -0,0 +1,30 @@
+aspect Computation {
+  syn String SenderRoot.basic() = getInput();
+  syn String SenderRoot.simple() = getInput() + "Post";
+  syn A SenderRoot.getSendNTA() {
+    A result = new A();
+    result.setValue(getInput());
+    Inner inner = new Inner();
+    inner.setInnerValue("1");
+    result.setInner(inner);
+    return result;
+  }
+}
+aspect MakeCodeCompile {
+
+}
+aspect MakeCodeWork {
+
+}
+aspect NameResolution {
+  // overriding customID guarantees to produce the same JSON representation for equal lists
+  // otherwise, the value for id is different each time
+  @Override
+  protected String A.customID() {
+    return getClass().getSimpleName() + getValue();
+  }
+  @Override
+  protected String Inner.customID() {
+    return getClass().getSimpleName() + getInnerValue();
+  }
+}
diff --git a/ragconnect.tests/src/test/01-input/java/Test.relast b/ragconnect.tests/src/test/01-input/java/Test.relast
new file mode 100644
index 0000000..13c7ba3
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/java/Test.relast
@@ -0,0 +1,9 @@
+Root ::= SenderRoot* ReceiverRoot;
+SenderRoot ::= <Input> <SendToken> SendNode:A SendManyNode:A* /SendNTA:A/ ;
+ReceiverRoot ::=
+    <SomeToken>
+    SomeNode:A
+    SomeNodeWithMapping:A
+    ManyNode:A*;
+A ::= <Value> Inner ;
+Inner ::= <InnerValue> ;
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java
new file mode 100644
index 0000000..2d22b3c
--- /dev/null
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/JavaTest.java
@@ -0,0 +1,43 @@
+package org.jastadd.ragconnect.tests;
+
+import javaInc.ast.Root;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Testing the Java handler.
+ *
+ * @author rschoene - Initial contribution
+ */
+@Tag("New")
+public class JavaTest {
+
+  protected Logger logger = LoggerFactory.getLogger(getClass());
+  private Root model;
+
+  void createModel() {
+    model = new Root();
+  }
+
+  @Test
+  public void testCommunicateSendInitialValue() {
+    createModel();
+  }
+
+  @Test
+  public void testCommunicateOnlyUpdatedValue() {
+    createModel();
+  }
+
+  @AfterEach
+  public void alwaysCloseConnections() {
+    logger.debug("Closing connections");
+    if (model != null) {
+      model.ragconnectCloseConnections();
+    }
+  }
+
+}
-- 
GitLab