diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ec7931d614a55e9107df68de039b151b1b0a65dd..667c03eba3d066f8b58643e62ee6bfb222a91014 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,7 @@ build: image: openjdk:11 stage: build script: - - ./gradlew --console=plain --no-daemon assemble jar + - ./gradlew --console=plain --no-daemon assemble testClasses jar artifacts: paths: - "ragconnect.base/build/libs/ragconnect-*.jar" diff --git a/pages/docs/compiler.md b/pages/docs/compiler.md index ccff6d6c05f7544ff75caeda4f19a6f87c765ad9..93b0bc0f48ec7e1e6083fe97236c5a8701057135 100644 --- a/pages/docs/compiler.md +++ b/pages/docs/compiler.md @@ -5,21 +5,21 @@ Additional options are as follows. ## Table with available options -| Name | Required (Default) | Description | -|---|---|---| -| `--rootNode` | Yes | Root node in the base grammar. | -| `--protocols` | No (`mqtt`) | Protocols to enable, currently available: `java` (experimental), `mqtt`, `rest`. | -| `--printYaml` | No (false) | Print out YAML instead of generating files. | -| `--verbose` | No (false) | Print more messages while compiling. | -| `--logReads` | No (false) | Enable logging for every received message. | -| `--logWrites` | No (false) | Enable logging for every sent message. | -| `--logIncremental` | No (false) | Enable logging for observer in incremental dependency tracking. | -| `--logTarget` | No (`console`) | Logging target to use, currently available: `console, slf4j`. | -| `--experimental-jastadd-329` | No (false) | Use tracing events `INC_FLUSH_START` and `INC_FLUSH_END` ([JastAdd issue #329][jastadd-issue-329]), see [section about automatic dependency tracking](using.md#dependency-tracking-automatically-derived). | -| `--incremental` | No (false) | Enables incremental dependency tracking (if `tracing` is also set appropriately). | -| `--tracing[=flush]` | No (false) | Enables incremental dependency tracking (if `incremental` is also set appropriately). | -| `--version` | No (false) | Print version info and exit (reused JastAdd option) | -| `--o` | No (`.`) | Output directory (reused JastAdd option) | +| Name | Required (Default) | Description | +|------------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--rootNode` | Yes | Root node in the base grammar. | +| `--protocols` | No (`mqtt`) | Protocols to enable, one of `java` (experimental), `mqtt`, `rest`, see [Handlers][handlers] for details. | +| `--printYaml` | No (false) | Print out YAML instead of generating files. | +| `--verbose` | No (false) | Print more messages while compiling. | +| `--logReads` | No (false) | Enable logging for every received message. | +| `--logWrites` | No (false) | Enable logging for every sent message. | +| `--logIncremental` | No (false) | Enable logging for observer in incremental dependency tracking. | +| `--logTarget` | No (`console`) | Logging target to use, currently available: `console, slf4j`. | +| `--experimental-jastadd-329` | No (false) | Use tracing events `INC_FLUSH_START` and `INC_FLUSH_END` ([JastAdd issue #329][jastadd-issue-329]), see [section about automatic dependency tracking][automatic-dependency-tracking]. | +| `--incremental` | No (false) | Enables incremental dependency tracking (if `tracing` is also set appropriately). | +| `--tracing[=flush]` | No (false) | Enables incremental dependency tracking (if `incremental` is also set appropriately). | +| `--version` | No (false) | Print version info and exit (reused JastAdd option) | +| `--o` | No (`.`) | Output directory (reused JastAdd option) | All files to be processed have to be passed as arguments. Their type is deduced by the file extension (`ast` and `relast` for input grammars, `connect` and `ragconnect` for RagConnect definitions file). @@ -29,44 +29,6 @@ Their type is deduced by the file extension (`ast` and `relast` for input gramma Using RagConnect itself does not introduce dependencies. However, depending on the selected protocols and/or used features, additional dependencies are required when using the generated code. -### Communication protocol characteristics - -#### Java - -- Protocol identifier: `java` -- URI scheme: `java://<ignored-host>[:ignored-port]/<topic>` - - the value for host and port are always ignored, but are necessary to form a legal URI -- No required runtime dependencies -- Additional remarks: - - First leading slash not included in topic. - - Currently, the default mappings are applied, which requires a consumer to expect `byte[]` (instead of a more intuitive token or node value). This might change in future versions. - -#### MQTT - -- Protocol identifier: `mqtt` -- URI scheme: `mqtt://<broker-host>[:port]/<topic>` -- Default port: 1883 -- Type for mapping definitions: `byte[]` -- Required runtime dependencies: - - `group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'` -- Additional remarks: - - First leading slash not included in topic. - - Mqtt is selected by default, so this dependency therefore is required "by default". - - Might work with other versions of `org.fusesource.mqtt-client.mqtt.client` as well. - -#### REST - -- Protocol identifier: `rest` -- URI scheme: `rest://localhost[:port]/<path>` -- Default port: 4567 -- Type for mapping definitions: `String` -- Required runtime dependencies: - - `group: 'com.sparkjava', name: 'spark-core', version: '2.9.3'` -- Additional remarks: - - Host is always `localhost`. - - Might work with newer versions of `com.sparkjava.spark-core` as well. - - For debugging, it is beneficial to include an implementation for [SLF4J][slf4j]. - ### Used features #### Automatic dependency tracking @@ -116,4 +78,5 @@ However, depending on the selected protocols and/or used features, additional de - Additionally, a slf4j binding is required, see [the slf4j user manual][slf4j] [jastadd-issue-329]: https://bitbucket.org/jastadd/jastadd2/issues/329/add-event-for-completion-of-flush -[slf4j]: https://www.slf4j.org/manual.html +[automatic-dependency-tracking]: using.md#dependency-tracking-automatically-derived +[handlers]: handlers.md diff --git a/pages/docs/extending.md b/pages/docs/extending.md index 14fa8c8dbb525900f26c0ccbf9f7b45ac17544e5..4ed4ce1285b402a747f69f59580310a10c037a92 100644 --- a/pages/docs/extending.md +++ b/pages/docs/extending.md @@ -5,9 +5,9 @@ To add a new communication protocol, the following locations have to be changed ### Within `ragconnect.base/src/main/resources` {% raw %} -- Add a new handler `ABCHandler.jadd`, similar to the existing handlers. +- Add a new handler `ABCHandler.jadd`, similar to the existing handlers. A handler must have a constructor accepting a single String parameter, and must have a `close()` method cleaning up any held resources. - In `handler.mustache`, add further methods if needed for handler usage in the application code (similar to `{{rootNodeName}}.{{SetupWaitUntilReadyMethodName}}` for `mqtt`) -- In `receiveDefinition.mustache` and `sendDefinition.mustache`: add a new case in the switch statements defining the logic to happen upon connect and disconnect for both definitions. If the new protocol is close to a PUSH semantic, follow `mqtt`. If it is closer to PULL semantic, follow `rest`. +- In `receiveDefinition.mustache` and `sendDefinition.mustache`: add a new case in the switch statements defining the logic to happen upon connect and disconnect for both definitions (that are four distinct locations). If the new protocol is close to a PUSH semantic, follow `mqtt`. If it is closer to PULL semantic, follow `restClient`. {% endraw %} ### Within `ragconnect.base/src/main/jastadd` @@ -18,7 +18,7 @@ In `Handlers.jrag`: Add a new attribute `RagConnect.abcHandler()` returning the In `Compiler.java`: - Add a new choice for `--protocols` similar to the existing ones -- Add a newly constructed handler in `setConfiguration` with the needed fields (definition file name within `resources` directory, commonly `ABCHandler.jadd`; class name of the handler; unique name for the protocol; whether the handler is used, i.e., if it was given in `--protocols`) +- Add a newly constructed handler in `setConfiguration` with the needed fields (class name of the handler; unique name for the protocol (must be a valid Java identifier); whether the handler is used, i.e., if it was given in `--protocols`) Furthermore, new test cases are appreciated, see [below](#writing-tests). diff --git a/pages/docs/handlers.md b/pages/docs/handlers.md new file mode 100644 index 0000000000000000000000000000000000000000..f126ea126b73c7568f9cf753eb712d6c2f80ed8e --- /dev/null +++ b/pages/docs/handlers.md @@ -0,0 +1,70 @@ +# Communication Protocol Characteristics (Handlers) + +## Java (experimental) + +Uses Java methods to supply values (receive) and for callbacks (send). + +- Protocol identifier: `java` +- URI scheme: `java://<ignored-host>[:ignored-port]/<topic>` + - the value for host and port are always ignored, but are necessary to form a legal URI +- No required runtime dependencies +- Receive behaviour: Use the generated method `ragconnectJavaPush` to pass a value to the receiving port. +- Send behaviour: When the value to be sent changes, previously registered callbacks are invoked. +- Additional remarks: + - First leading slash not included in topic. + - Currently, the default mappings are applied, which requires a consumer to expect `byte[]` (instead of a more intuitive token or node value). This might change in future versions. + + +## MQTT + +Use an MQTT broker to receive and send messages. + +- Protocol identifier: `mqtt` +- URI scheme: `mqtt://<broker-host>[:port]/<topic>` +- Default port: 1883 +- Type for mapping definitions: `byte[]` +- Required runtime dependencies: + - `group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version: '1.15'` +- Receive behaviour: Upon connection, instruct the MQTT broker to listen for messages on some topic and pass the value of those messages to the receiving port. +- Send behaviour: When the value to be sent changes, publish a message to the topic specified upon connection. +- Additional remarks: + - First leading slash not included in topic. + - Mqtt is selected by default, so this dependency therefore is required "by default". + - Might work with other versions of `org.fusesource.mqtt-client.mqtt.client` as well. + + +## REST Server + +Create a new REST server with its own target routes. + +- Protocol identifier: `rest` +- URI scheme: `rest://localhost[:port]/<path>` +- Default port: 4567 +- Type for mapping definitions: `byte[]` +- Required runtime dependencies: + - `group: 'com.sparkjava', name: 'spark-core', version: '2.9.3'` +- Receive behaviour: Upon connection, create a new PUT connection and pass the value of every call to this PUT route to the receiving port. +- Send behaviour: Upon connection, create a new GET connection and serve the latest value at this GET route. +- Additional remarks: + - Host is always `localhost`. + - Targets to be invoked need to replace `rest` with `http` + - Might work with newer versions of `com.sparkjava.spark-core` as well. + - For debugging, it is beneficial to include an implementation for [SLF4J][slf4j]. + + +## REST Client + +Invoke REST routes to fetch and send values. + +- Protocol identifier: `restClient` +- URI scheme: `restClient://localhost[:port]/<path>` +- Default port: 80 +- Type for mapping definitions: `byte[]` +- No required runtime dependencies +- Receive behaviour: Whenever the accessor is called, a GET request fetches the latest data and returns this data. +- Send behaviour: When the value to be sent changes, a PUT request sends this data. +- Additional remarks: + - Invoked target replaces `restClient` with `http` + - **Important constraint**: Receiving ports are only supported for tokens, since they interrupt the getter method! + +[slf4j]: https://www.slf4j.org/manual.html diff --git a/pages/mkdocs.yml b/pages/mkdocs.yml index 15c561496d48591b4d5b66049646633b142fe3ef..21b10b06e5bb470225199e1f3b7481e72fb55001 100644 --- a/pages/mkdocs.yml +++ b/pages/mkdocs.yml @@ -7,6 +7,7 @@ nav: - "Using RagConnect (by Example)": using.md - "RagConnect Specification Language": dsl.md - "Compiler options": compiler.md + - "Communication Protocols": handlers.md - "Use Cases": use_cases.md - "Inner workings": inner-workings.md - "Evaluation Metrics: Lines of Code": cloc.md diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag index e1ec0f28977748a6b2d896c0c82c58c116f0081c..407efa363aa6208730add0899d650a2cd73a497a 100644 --- a/ragconnect.base/src/main/jastadd/Analysis.jrag +++ b/ragconnect.base/src/main/jastadd/Analysis.jrag @@ -112,7 +112,7 @@ aspect Analysis { return !getDependencySourceDefinitionList().isEmpty() || getTokenPortTargetList().stream() .map(PortTarget::containingPortDefinition) - .anyMatch(PortDefinition::shouldNotResetValue); + .anyMatch(def -> def.shouldNotResetValue() || ragconnect().restClientHandler().getInUse()); } // --- effectiveUsedAt --- diff --git a/ragconnect.base/src/main/jastadd/Handlers.jrag b/ragconnect.base/src/main/jastadd/Handlers.jrag index e64a8c5d0db5467f9a4b0d7451e8c38ab22a8bbf..8d891d5025877f387425803fa39e930a7b023314 100644 --- a/ragconnect.base/src/main/jastadd/Handlers.jrag +++ b/ragconnect.base/src/main/jastadd/Handlers.jrag @@ -2,6 +2,7 @@ aspect RagConnectHandlers { syn Handler RagConnect.javaHandler() = resolveHandlerByName("java"); syn Handler RagConnect.mqttHandler() = resolveHandlerByName("mqtt"); syn Handler RagConnect.restHandler() = resolveHandlerByName("rest"); + syn Handler RagConnect.restClientHandler() = resolveHandlerByName("restClient"); private Handler RagConnect.resolveHandlerByName(String uniqueName) { for (Handler handler : getHandlerList()) { diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd index 60fe493797256563e7bfca17a6c35d341acfb8c0..ebf3b0af00dcb9f6acf7dd95cd658cdb0ead5a49 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.jadd +++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd @@ -232,7 +232,8 @@ aspect MustacheMappingApplicationAndDefinition { eq MRelationSendDefinition.preemptiveReturn() = "return false;"; eq MTokenReceiveDefinition.firstInputVarName() = "message"; - eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall(); + // if receiverName variable is set, use internal getter instead to avoid StackOverflow + eq MTokenReceiveDefinition.preemptiveExpectedValue() = "(" + getPortDefinition().receiverName() + " != null ? get" + getPortDefinition().token().internalName() + "() : " + getterMethodCall() + ")"; eq MTokenReceiveDefinition.preemptiveReturn() = "return;"; eq MTokenSendDefinition.firstInputVarName() = getterMethodCall(); @@ -525,6 +526,7 @@ aspect MustacheSendDefinition { syn boolean PortDefinition.relationPortWithListRole() = getPortTarget().relationPortWithListRole(); + syn String PortDefinition.receiverName() = getPortTarget().receiverName(); syn String PortDefinition.senderName() = getPortTarget().senderName(); syn java.util.List<SendIncrementalObserverEntry> PortDefinition.sendIncrementalObserverEntries() { @@ -583,6 +585,9 @@ containingPortDefinition().getIndexBasedListAccess()); syn boolean PortTarget.relationPortWithListRole() = false; eq RelationPortTarget.relationPortWithListRole() = getRole().isListRole(); + syn String PortTarget.receiverName() = ragconnect().internalRagConnectPrefix() + "_receiver_" + entityName(); + eq ContextFreeTypePortTarget.receiverName() = null; + syn String PortTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName(); eq ContextFreeTypePortTarget.senderName() = null; @@ -645,6 +650,16 @@ aspect MustacheTokenComponent { syn String TokenComponent.javaType() = effectiveJavaTypeUse().prettyPrint(); + syn PortDefinition TokenComponent.normalTokenReceiveDef() { + for (Component comp : meOwnedByOthers()) { + PortDefinition maybeResult = comp.asTokenComponent().directNormalTokenReceiveDef(); + if (maybeResult != null) { + return maybeResult; + } + } + return directNormalTokenReceiveDef(); + } + syn PortDefinition TokenComponent.normalTokenSendDef() { for (Component comp : meOwnedByOthers()) { PortDefinition maybeResult = comp.asTokenComponent().directNormalTokenSendDef(); @@ -682,6 +697,16 @@ aspect MustacheTokenComponent { // > see MustacheSend for updateMethodName, writeMethodName // === attributes needed for computing above ones === + syn PortDefinition TokenComponent.directNormalTokenReceiveDef() { + for (PortTarget target : getTokenPortTargetList()) { + if (target.isTokenPortTarget() && !target.containingPortDefinition().getSend() && + ragconnect().restClientHandler().getInUse()) { + return target.containingPortDefinition(); + } + } + return null; + } + syn PortDefinition TokenComponent.directNormalTokenSendDef() { for (PortTarget target : getTokenPortTargetList()) { if (target.isTokenPortTarget() && target.containingPortDefinition().shouldNotResetValue()) { diff --git a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag index 5aa0f8c5fb1d007e6dfaca1b0fd70cf7566d810b..c02518e3658395beffa69c3f5ba6983b826de62d 100644 --- a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag +++ b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag @@ -233,6 +233,15 @@ aspect IntermediateToYAML { } return sb; } + + // FIXME: remove refine once fixed in upstream mustache + refine Helpers protected SimpleElement ComplexElement.makeStringElement(String value) { + // simple test, check for special characters + return containsAny(value, ":#,[{\"\n") ? + StringElement.of(value.replace("\n", "\\n").replace("\"", "\\\"")) : + ValueElement.of(value); + } + } aspect Navigation { 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 be81bfd4ea84123624e97c0047b85e7b29bda348..173404cee21fdb497276bc99c169dbe0c5864e58 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 @@ -39,6 +39,7 @@ public class Compiler extends AbstractCompiler { private static final String OPTION_PROTOCOL_JAVA = "java"; private static final String OPTION_PROTOCOL_MQTT = "mqtt"; private static final String OPTION_PROTOCOL_REST = "rest"; + private static final String OPTION_PROTOCOL_REST_CLIENT = "restClient"; public Compiler() { super("ragconnect", true); @@ -183,6 +184,7 @@ public class Compiler extends AbstractCompiler { .addDefaultValue(OPTION_PROTOCOL_MQTT, "Enable MQTT") .addAcceptedValue(OPTION_PROTOCOL_JAVA, "Enable Java (experimental)") .addAcceptedValue(OPTION_PROTOCOL_REST, "Enable REST") + .addAcceptedValue(OPTION_PROTOCOL_REST_CLIENT, "Enable REST client (experimental)") ); optionPrintYaml = addOption( new BooleanOption("printYaml", "Print out YAML instead of generating files and exit.") @@ -341,6 +343,8 @@ public class Compiler extends AbstractCompiler { 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))); + ragConnect.addHandler(new Handler("RestClientHandler", "restClient", + optionProtocols.hasValue(OPTION_PROTOCOL_REST_CLIENT))); } public String generateAspect(RagConnect ragConnect) { diff --git a/ragconnect.base/src/main/resources/RestClientHandler.mustache b/ragconnect.base/src/main/resources/RestClientHandler.mustache new file mode 100644 index 0000000000000000000000000000000000000000..24307ef7c8e22274bf71929774a8f7e12521b216 --- /dev/null +++ b/ragconnect.base/src/main/resources/RestClientHandler.mustache @@ -0,0 +1,65 @@ +public class RestClientHandler { + private java.net.http.HttpClient httpClient; + public RestClientHandler(String name) { + httpClient = java.net.http.HttpClient.newHttpClient(); + } + + public RagConnectReceiver newReceiverFor(RagConnectToken connectToken, java.util.function.BiConsumer<String, byte[]> callback) { + java.net.URI target = java.net.URI.create(connectToken.uri.toString().replaceFirst("restClient", "http")); + java.net.http.HttpRequest httpRequest = java.net.http.HttpRequest.newBuilder(target) + .GET() + .build(); + return () -> { + byte[] rawInput; + try { + rawInput = fetchFrom(httpRequest); + } catch (Exception e) { + {{logException}}("Exception when fetching from " + target, e); + return; + } + callback.accept("", rawInput); + }; + } + + byte[] fetchFrom(java.net.http.HttpRequest httpRequest) throws java.io.IOException, InterruptedException { + return httpClient.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofByteArray()).body(); + } + + public RestClientPreparedRequest preparePut(java.net.URI uri) { + java.net.URI target = java.net.URI.create(uri.toString().replaceFirst("restClient", "http")); + java.net.http.HttpRequest.Builder httpRequestBuilder = java.net.http.HttpRequest.newBuilder(target); + return new RestClientPreparedRequest(httpRequestBuilder); + } + + void sendRequestAsync(RestClientPreparedRequest preparedRequest, byte[] message) { + java.net.http.HttpRequest httpRequest = preparedRequest.builder + .PUT(java.net.http.HttpRequest.BodyPublishers.ofByteArray(message)) + .build(); + httpClient.sendAsync(httpRequest, java.net.http.HttpResponse.BodyHandlers.discarding()); + } + + void sendRequestSync(RestClientPreparedRequest preparedRequest, byte[] message) { + java.net.http.HttpRequest httpRequest = preparedRequest.builder + .PUT(java.net.http.HttpRequest.BodyPublishers.ofByteArray(message)) + .build(); + java.net.http.HttpResponse response; + try { + response = httpClient.send(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofString()); + {{logDebug}}("Response for message to {{log_}} is {{log_}} {{log_}}", + httpRequest, response.statusCode(), response.body()); + } catch (java.io.IOException | InterruptedException e) { + {{logException}}("Exception while sending {{log_}}: {{log_}}", httpRequest, e); + } + } + + public void close() { + // empty + } +} + +public class RestClientPreparedRequest { + java.net.http.HttpRequest.Builder builder; + RestClientPreparedRequest(java.net.http.HttpRequest.Builder builder) { + this.builder = builder; + } +} diff --git a/ragconnect.base/src/main/resources/RestHandler.mustache b/ragconnect.base/src/main/resources/RestHandler.mustache index 25192af366bfd4da29ace5c11d661b8cc8fd0992..4e8f228712fd7b6e6ee3c66e72f85c8d8ac5dd9b 100644 --- a/ragconnect.base/src/main/resources/RestHandler.mustache +++ b/ragconnect.base/src/main/resources/RestHandler.mustache @@ -20,13 +20,13 @@ public class RestServerHandler { return handler; } - public boolean newPUTConnection(RagConnectToken connectToken, java.util.function.Consumer<String> callback) { - tokensForRemoval.put(connectToken, callback); - resolveHandler(connectToken.uri).newPUTConnection(connectToken.uri.getPath(), callback); + public boolean newPUTConnection(RagConnectToken connectToken, java.util.function.BiConsumer<String, byte[]> consumer) { + tokensForRemoval.put(connectToken, consumer); + resolveHandler(connectToken.uri).newPUTConnection(connectToken.uri.getPath(), consumer); return true; } - public boolean newGETConnection(RagConnectToken connectToken, SupplierWithException<String> supplier) { + public boolean newGETConnection(RagConnectToken connectToken, SupplierWithException<byte[]> supplier) { tokensForRemoval.put(connectToken, supplier); resolveHandler(connectToken.uri).newGETConnection(connectToken.uri.getPath(), supplier); return true; @@ -52,8 +52,8 @@ public class RestHandler { private int port; /** Dispatch knowledge */ - private final java.util.Map<String, java.util.List<java.util.function.Consumer<String>>> callbacks; - private final java.util.Map<String, SupplierWithException<String>> suppliers; + private final java.util.Map<String, java.util.List<java.util.function.BiConsumer<String, byte[]>>> callbacks; + private final java.util.Map<String, SupplierWithException<byte[]>> suppliers; public RestHandler() { this.port = DEFAULT_PORT; @@ -64,23 +64,24 @@ public class RestHandler { public RestHandler setPort(int port) { this.port = port; start(); + spark.Spark.path("", () -> spark.Spark.before((request, response) -> {{logDebug}}("Request on {{log_}}: {{log_}}", request.pathInfo(), request.bodyAsBytes()))); return this; } - public void newPUTConnection(String path, java.util.function.Consumer<String> callback) { + public void newPUTConnection(String path, java.util.function.BiConsumer<String, byte[]> callback) { if (callbacks.containsKey(path)) { callbacks.get(path).add(callback); } else { // setup path - java.util.List<java.util.function.Consumer<String>> callbackList = new java.util.ArrayList<>(); + java.util.List<java.util.function.BiConsumer<String, byte[]>> callbackList = new java.util.ArrayList<>(); callbackList.add(callback); callbacks.put(path, callbackList); spark.Spark.put(path, (request, response) -> { - String content = request.body(); + byte[] content = request.bodyAsBytes(); java.util.Set<String> errors = new java.util.HashSet<>(); - for (java.util.function.Consumer<String> f : callbackList) { + for (java.util.function.BiConsumer<String, byte[]> f : callbackList) { try { - f.accept(content); + f.accept(path, content); } catch (Exception e) { errors.add(e.getMessage()); } @@ -94,7 +95,7 @@ public class RestHandler { } } - public void newGETConnection(String path, SupplierWithException<String> supplier) { + public void newGETConnection(String path, SupplierWithException<byte[]> supplier) { if (suppliers.get(path) != null) { {{logWarn}}("Overriding existing supplier for '{{log_}}'", path); } diff --git a/ragconnect.base/src/main/resources/handler.mustache b/ragconnect.base/src/main/resources/handler.mustache index 75bd14ce26638ceeee267251988a01b516de0970..435a88c8c765ed35fe72caeca96ca1770b3f0968 100644 --- a/ragconnect.base/src/main/resources/handler.mustache +++ b/ragconnect.base/src/main/resources/handler.mustache @@ -46,6 +46,12 @@ aspect RagConnectHandler { {{/InUse}} {{/restHandler}} +{{#restClientHandler}} + {{#InUse}} + {{> RestClientHandler}} + {{/InUse}} +{{/restClientHandler}} + class RagConnectToken { static java.util.concurrent.atomic.AtomicLong counter = new java.util.concurrent.atomic.AtomicLong(0); final long id; @@ -191,4 +197,7 @@ aspect RagConnectHandler { java.util.Optional.ofNullable(publishers.get(index)).ifPresent(publisher -> publisher.setLastValue(value)); } } + + interface RagConnectReceiver extends Runnable { + } } diff --git a/ragconnect.base/src/main/resources/receiveDefinition.mustache b/ragconnect.base/src/main/resources/receiveDefinition.mustache index b2e05ce6b2ce246b5225b98eecc7779bf3b0cfa0..f1ca58d6a02ba1dcfd3b47cb62e1daa6c92fd195 100644 --- a/ragconnect.base/src/main/resources/receiveDefinition.mustache +++ b/ragconnect.base/src/main/resources/receiveDefinition.mustache @@ -1,3 +1,5 @@ +private RagConnectReceiver {{parentTypeName}}.{{receiverName}} = null; + {{#typeIsList}} {{#IndexBasedListAccess}} private int {{parentTypeName}}.{{resolveInListMethodName}}(String topic) { @@ -116,13 +118,18 @@ private boolean {{parentTypeName}}.{{internalConnectMethodName}}(String {{connec {{#restHandler}} {{#InUse}} case "rest": - success = {{attributeName}}().newPUTConnection(connectToken, input -> { - // TODO wildcard-topic not supported yet - consumer.accept("", input.getBytes()); - }); + success = {{attributeName}}().newPUTConnection(connectToken, consumer); break; {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": + {{receiverName}} = {{attributeName}}().newReceiverFor(connectToken, consumer); + success = {{receiverName}} != null; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}'.", scheme); success = false; @@ -161,6 +168,15 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam break; {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": disconnectingMethod = token -> { + {{receiverName}} = null; + return true; + }; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}' in '{{log_}}' for disconnecting {{parentTypeName}}.{{entityName}}", scheme, {{connectParameterName}}); diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache index d97618567dce78a10b56680aac09d31e5b06c74a..45fa4de131a4331827e2643d04d51757c61e1fb2 100644 --- a/ragconnect.base/src/main/resources/sendDefinition.mustache +++ b/ragconnect.base/src/main/resources/sendDefinition.mustache @@ -54,12 +54,28 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete case "rest": { success = {{attributeName}}().newGETConnection(connectToken, () -> { {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); - return new String({{lastValueGetterCall}}); + return {{lastValueGetterCall}}; }); break; } {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": + RestClientPreparedRequest preparedRequest = {{attributeName}}().preparePut(uri); + {{senderName}}.add(() -> { + {{! sync or async could be selected via options, see issue #62 }} + {{attributeName}}().sendRequestSync(preparedRequest, {{lastValueGetterCall}}); + }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken); + {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}}); + if (writeCurrentValue) { + {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken); + } + success = true; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}'.", scheme); success = false; @@ -121,6 +137,13 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam break; {{/InUse}} {{/restHandler}} + {{#restClientHandler}} + {{#InUse}} + case "restClient": + disconnectingMethod = {{senderName}}::remove; + break; + {{/InUse}} + {{/restClientHandler}} default: {{logError}}("Unknown protocol '{{log_}}' in '{{log_}}' for disconnecting {{parentTypeName}}.{{entityName}}", scheme, {{connectParameterName}}); @@ -165,9 +188,6 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i {{#needForwarding}} syn {{{forwardingType}}} {{parentTypeName}}.{{forwardingName}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) { {{#relationPortWithListRole}} -// for (var element : {{realGetterMethodCall}}) { -// element.{{touchedTerminalsMethodName}}(); -// } {{realGetterMethodCall}}.stream().forEach(element -> element.{{touchedTerminalsMethodName}}()); return {{realGetterMethodCall}}; {{/relationPortWithListRole}} diff --git a/ragconnect.base/src/main/resources/tokenComponent.mustache b/ragconnect.base/src/main/resources/tokenComponent.mustache index 3d90c34b5ccfd3844972ef535844f8b042e1e3f1..ee669d87bf128bcecdd2a2830586bd26df83921f 100644 --- a/ragconnect.base/src/main/resources/tokenComponent.mustache +++ b/ragconnect.base/src/main/resources/tokenComponent.mustache @@ -18,5 +18,10 @@ public {{parentTypeName}} {{parentTypeName}}.set{{Name}}({{javaType}} value) { } public {{javaType}} {{parentTypeName}}.get{{Name}}() { + {{#normalTokenReceiveDef}} + if ({{receiverName}} != null) { + {{receiverName}}.run(); + } + {{/normalTokenReceiveDef}} return get{{internalName}}(); } diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle index fb5e710ae4d4873e37e4454c0adccccc26750838..255091abca95394c7245be0d67564cd08d5a5328 100644 --- a/ragconnect.tests/build.gradle +++ b/ragconnect.tests/build.gradle @@ -712,6 +712,28 @@ task compileJavaIncremental(type: RagConnectTest) { } } +// --- Test: rest-client-server --- +task compileRestClientServerTest(type: RagConnectTest) { + ragconnect { + outputDir = file('src/test/02-after-ragconnect/restClientServer') + inputFiles = [file('src/test/01-input/restClientServer/Test.relast'), + file('src/test/01-input/restClientServer/Test.connect')] + rootNode = 'Root' + protocols = ['rest','restClient'] + extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329']) + } + relast { + useJastAddNames = true + grammarName = 'src/test/03-after-relast/restClientServer/restClientServer' + serializer = 'jackson' + } + jastadd { + jastAddList = 'JastAddList' + packageName = 'restClientServer.ast' + extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL + } +} + // --- Task order --- classes.dependsOn(':ragconnect.base:jar') diff --git a/ragconnect.tests/src/test/01-input/restClientServer/Test.connect b/ragconnect.tests/src/test/01-input/restClientServer/Test.connect new file mode 100644 index 0000000000000000000000000000000000000000..cbee90ac4a7e4bcc2209035a1e77b75abf16d2d2 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/restClientServer/Test.connect @@ -0,0 +1,5 @@ +send SenderRoot.SimpleValue ; +receive ReceiverRoot.ReceivedValue ; + +send SenderRoot.SingleA ; +receive ReceiverRoot.SomeA ; diff --git a/ragconnect.tests/src/test/01-input/restClientServer/Test.relast b/ragconnect.tests/src/test/01-input/restClientServer/Test.relast new file mode 100644 index 0000000000000000000000000000000000000000..5c7bcdeb79b3df51ec142093e7b920b0e04e8ae4 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/restClientServer/Test.relast @@ -0,0 +1,5 @@ +Root ::= SenderRoot ReceiverRoot; +SenderRoot ::= <SimpleValue:int> SingleA:A ; +ReceiverRoot ::= <ReceivedValue:int> SomeA:A ; +A ::= <Value> Inner ; +Inner ::= <InnerValue> ; diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bcfaea91118d671f8a65275e1abcf364862036a0 --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RestClientServerTest.java @@ -0,0 +1,124 @@ +package org.jastadd.ragconnect.tests; + +import org.jastadd.ragconnect.tests.utils.TestChecker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import restClientServer.ast.*; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Testing RestClient. + * + * @author rschoene - Initial contribution + */ +public class RestClientServerTest extends RagConnectTest { + private Root root; + private TestChecker checker; + + private static final String TOPIC_NT_A_VALUE = "nt/a/value"; + private static final String TOPIC_NT_A_INNER_VALUE = "nt/a/inner/value"; + + @Test + public void testSimpleSenderRest() throws IOException { + root.getSenderRoot().setSimpleValue(1); + + assertThat(root.getReceiverRoot().connectReceivedValue("restClient://localhost:4567/serve-simple")).isTrue(); + assertThat(root.getSenderRoot().connectSimpleValue("rest://localhost:4567/serve-simple", true)).isTrue(); + + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(1); + + root.getSenderRoot().setSimpleValue(2); + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(2); + } + + @Test + public void testSimpleSenderRestClient() throws IOException { + root.getSenderRoot().setSimpleValue(1); + + assertThat(root.getReceiverRoot().connectReceivedValue("rest://localhost:4567/put-simple")).isTrue(); + assertThat(root.getSenderRoot().connectSimpleValue("restClient://localhost:4567/put-simple", true)).isTrue(); + + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(1); + + root.getSenderRoot().setSimpleValue(2); + assertThat(root.getReceiverRoot().getReceivedValue()).isEqualTo(2); + } + + @Test + public void testNonterminalSenderRest() throws IOException { + A a = new A().setValue("a1"); + a.setInner(new Inner().setInnerValue("1")); + root.getSenderRoot().setSingleA(a); + + checker.put(TOPIC_NT_A_VALUE, "a1") + .put(TOPIC_NT_A_INNER_VALUE, "1"); + + assertThat(root.getReceiverRoot().connectSomeA("rest://localhost:4567/put-nt")).isTrue(); + assertThat(root.getSenderRoot().connectSingleA("restClient://localhost:4567/put-nt", true)).isTrue(); + + communicateNonterminal(); + } + + @Test + @Disabled("Receiving nonterminals using restClient is not supported yet") + public void testNonterminalSenderRestClient() throws IOException { + A a = new A().setValue("a1"); + a.setInner(new Inner().setInnerValue("1")); + root.getSenderRoot().setSingleA(a); + + assertThat(root.getReceiverRoot().connectSomeA("restClient://localhost:4567/serve-nt")).isTrue(); + assertThat(root.getSenderRoot().connectSingleA("rest://localhost:4567/serve-nt", true)).isTrue(); + + communicateNonterminal(); + } + + private void communicateNonterminal() { + A a = root.getSenderRoot().getSingleA(); + + checker.check(); + + a.setValue("a23"); + checker.put(TOPIC_NT_A_VALUE, "a23").check(); + + a.getInner().setInnerValue("abc"); + checker.put(TOPIC_NT_A_INNER_VALUE, "abc").check(); + } + + @BeforeEach + public void createModel() { + root = new Root(); + root.setReceiverRoot(new ReceiverRoot()); + root.setSenderRoot(new SenderRoot()); + + checker = new TestChecker().setActualNumberOfValues(() -> 0); + checker.setCheckForString(TOPIC_NT_A_VALUE, (name, expected) -> + assertThat(someAValue()).describedAs(name).isEqualTo(expected)) + .setCheckForString(TOPIC_NT_A_INNER_VALUE, (name, expected) -> + assertThat(someAInnerValueOrNull()).describedAs(name).isEqualTo(expected)) + ; + } + + private String someAValue() { + if (root.getReceiverRoot().getSomeA() == null) { + return null; + } + return root.getReceiverRoot().getSomeA().getValue(); + } + + private String someAInnerValueOrNull() { + if (root.getReceiverRoot().getSomeA() == null || root.getReceiverRoot().getSomeA().getInner() == null) { + return null; + } + return root.getReceiverRoot().getSomeA().getInner().getInnerValue(); + } + + @AfterEach + public void close() { + root.ragconnectCloseConnections(); + } +}