diff --git a/pom.xml b/pom.xml
index a5d320569..9408ad500 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
1.9.0
2.14.0
3.4.0
+ 0.3.1
diff --git a/spring-boot-examples/workflows/pom.xml b/spring-boot-examples/workflows/pom.xml
index 4711b799b..f28c48c0f 100644
--- a/spring-boot-examples/workflows/pom.xml
+++ b/spring-boot-examples/workflows/pom.xml
@@ -40,6 +40,12 @@
rest-assured
test
+
+ io.github.microcks
+ microcks-testcontainers
+ ${microcks.version}
+ test
+
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java
index 5ff98f886..45f08ca17 100644
--- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsConfiguration.java
@@ -13,9 +13,12 @@
package io.dapr.springboot.examples.wfp;
+import com.fasterxml.jackson.databind.ObjectMapper;
import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog;
+import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
@Configuration
public class WorkflowPatternsConfiguration {
@@ -23,4 +26,14 @@ public class WorkflowPatternsConfiguration {
public CleanUpLog cleanUpLog(){
return new CleanUpLog();
}
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplateBuilder().build();
+ }
+
+ @Bean
+ public ObjectMapper mapper() {
+ return new ObjectMapper();
+ }
}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
index 55695821d..ddffdb018 100644
--- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java
@@ -22,6 +22,8 @@
import io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow;
import io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow;
import io.dapr.springboot.examples.wfp.fanoutin.Result;
+import io.dapr.springboot.examples.wfp.remoteendpoint.Payload;
+import io.dapr.springboot.examples.wfp.remoteendpoint.RemoteEndpointWorkflow;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;
import org.slf4j.Logger;
@@ -53,6 +55,7 @@ public class WorkflowPatternsRestController {
private Map ordersToApprove = new HashMap<>();
+
/**
* Run Chain Demo Workflow
* @return the output of the ChainWorkflow execution
@@ -137,4 +140,17 @@ public CleanUpLog continueAsNew()
return workflowInstanceStatus.readOutputAs(CleanUpLog.class);
}
+ @PostMapping("wfp/remote-endpoint")
+ public Payload remoteEndpoint(@RequestBody Payload payload)
+ throws TimeoutException {
+
+ String instanceId = daprWorkflowClient.scheduleNewWorkflow(RemoteEndpointWorkflow.class, payload);
+ logger.info("Workflow instance " + instanceId + " started");
+
+ WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient
+ .waitForInstanceCompletion(instanceId, null, true);
+ System.out.printf("workflow instance with ID: %s completed.", instanceId);
+ return workflowInstanceStatus.readOutputAs(Payload.class);
+ }
+
}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java
new file mode 100644
index 000000000..a0636f550
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/CallRemoteEndpointActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.wfp.remoteendpoint;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+public class CallRemoteEndpointActivity implements WorkflowActivity {
+
+ private Logger logger = LoggerFactory.getLogger(CallRemoteEndpointActivity.class);
+
+ @Value("${application.process-base-url:}")
+ private String processBaseURL;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ logger.info("Starting Activity: " + ctx.getName());
+ var payload = ctx.getInput(Payload.class);
+
+ HttpEntity request =
+ new HttpEntity<>(payload);
+ payload = restTemplate.postForObject(processBaseURL + "/process", request, Payload.class);
+
+ logger.info("Payload from the remote service: " + payload);
+
+ return payload;
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java
new file mode 100644
index 000000000..fc1b6511d
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/Payload.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.wfp.remoteendpoint;
+
+public class Payload {
+ private String id;
+ private String content;
+ private Boolean processed = false;
+
+ public Payload(String id, String content) {
+ this.id = id;
+ this.content = content;
+ }
+
+ public Payload() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public Boolean getProcessed() {
+ return processed;
+ }
+
+ public void setProcessed(Boolean processed) {
+ this.processed = processed;
+ }
+
+ @Override
+ public String toString() {
+ return "Payload{" +
+ "id='" + id + '\'' +
+ ", content='" + content + '\'' +
+ ", processed=" + processed +
+ '}';
+ }
+}
diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java
new file mode 100644
index 000000000..75934e789
--- /dev/null
+++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/remoteendpoint/RemoteEndpointWorkflow.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package io.dapr.springboot.examples.wfp.remoteendpoint;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import io.dapr.workflows.WorkflowTaskOptions;
+import io.dapr.workflows.WorkflowTaskRetryPolicy;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+@Component
+public class RemoteEndpointWorkflow implements Workflow {
+
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow: " + ctx.getName());
+
+ Payload payload = ctx.getInput(Payload.class);
+ payload = ctx.callActivity(CallRemoteEndpointActivity.class.getName(), payload ,
+ new WorkflowTaskOptions(new WorkflowTaskRetryPolicy(5,
+ Duration.ofSeconds(2), 1.0, Duration.ofSeconds(10), Duration.ofSeconds(20))),
+ Payload.class).await();
+
+ ctx.getLogger().info("Workflow finished with result: " + payload);
+ ctx.complete(payload);
+ };
+ }
+}
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
index a0e3a087c..b27900652 100644
--- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
+++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java
@@ -15,11 +15,19 @@
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
+import io.github.microcks.testcontainers.MicrocksContainersEnsemble;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.springframework.test.context.DynamicPropertyRegistrar;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.Network;
import java.util.Collections;
+import java.util.List;
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
@@ -28,16 +36,66 @@ public class DaprTestContainersConfig {
@Bean
@ServiceConnection
- public DaprContainer daprContainer() {
+ public DaprContainer daprContainer(Network network) {
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
.withAppName("workflow-patterns-app")
.withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true))))
.withAppPort(8080)
+ .withNetwork(network)
.withAppHealthCheckPath("/actuator/health")
.withAppChannelAddress("host.testcontainers.internal");
}
+ @Bean
+ MicrocksContainersEnsemble microcksEnsemble(Network network) {
+ return new MicrocksContainersEnsemble(network, "quay.io/microcks/microcks-uber:1.11.2")
+ .withAccessToHost(true) // We need this to access our webapp while it runs
+ .withMainArtifacts("third-parties/remote-http-service.yaml");
+ }
+
+ @Bean
+ public DynamicPropertyRegistrar endpointsProperties(MicrocksContainersEnsemble ensemble) {
+ // We need to replace the default endpoints with those provided by Microcks.
+ return (properties) -> {
+ properties.add("application.process-base-url", () -> ensemble.getMicrocksContainer()
+ .getRestMockEndpoint("API Payload Processor", "1.0.0"));
+ };
+ }
+
+ @Bean
+ public Network getDaprNetwork(Environment env) {
+ boolean reuse = env.getProperty("reuse", Boolean.class, false);
+ if (reuse) {
+ Network defaultDaprNetwork = new Network() {
+ @Override
+ public String getId() {
+ return "dapr-network";
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return null;
+ }
+ };
+
+ List networks = DockerClientFactory.instance().client().listNetworksCmd()
+ .withNameFilter("dapr-network").exec();
+ if (networks.isEmpty()) {
+ Network.builder().createNetworkCmdModifier(cmd -> cmd.withName("dapr-network")).build().getId();
+ return defaultDaprNetwork;
+ } else {
+ return defaultDaprNetwork;
+ }
+ } else {
+ return Network.newNetwork();
+ }
+ }
}
diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
index 4ca36cb58..625a621a6 100644
--- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
+++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java
@@ -16,6 +16,8 @@
import io.dapr.client.DaprClient;
import io.dapr.springboot.DaprAutoConfiguration;
import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog;
+import io.dapr.springboot.examples.wfp.remoteendpoint.Payload;
+import io.github.microcks.testcontainers.MicrocksContainersEnsemble;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
@@ -39,6 +41,9 @@ class WorkflowPatternsAppTests {
@Autowired
private DaprClient daprClient;
+ @Autowired
+ private MicrocksContainersEnsemble ensemble;
+
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8080;
@@ -139,4 +144,20 @@ void testContinueAsNew() {
assertEquals(5, cleanUpLog.getCleanUpTimes());
}
+ @Test
+ void testRemoteEndpoint() {
+
+ Payload payload = given().contentType(ContentType.JSON)
+ .body(new Payload("123", "content goes here"))
+ .when()
+ .post("/wfp/remote-endpoint")
+ .then()
+ .statusCode(200).extract().as(Payload.class);
+
+ assertEquals(true, payload.getProcessed());
+
+ assertEquals(2, ensemble.getMicrocksContainer()
+ .getServiceInvocationsCount("API Payload Processor", "1.0.0"));
+ }
+
}
diff --git a/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml b/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml
new file mode 100644
index 000000000..59819a282
--- /dev/null
+++ b/spring-boot-examples/workflows/src/test/resources/third-parties/remote-http-service.yaml
@@ -0,0 +1,87 @@
+---
+openapi: 3.0.2
+info:
+ title: API Payload Processor
+ version: 1.0.0
+ description: API definition of API Payload Processor sample app
+ contact:
+ name: Salaboy
+ url: http://github.com/salaboy
+ email: salaboy@gmail.com
+ license:
+ name: MIT License
+ url: https://opensource.org/licenses/MIT
+paths:
+ /process:
+ summary: Process payload
+ post:
+ tags:
+ - process
+ x-microcks-operation:
+ dispatcher: SCRIPT
+ dispatcherRules: |
+ def retries = store.get("retries") ?:"first"
+ if (retries == "first") {
+ store.put("retries", "second", 60)
+ return "Error"
+ }
+ store.delete("retries")
+ return "Payload"
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Payload'
+ required: true
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Payload'
+ examples:
+ Payload:
+ value:
+ id: 123
+ content: payload content here
+ processed: true
+ description: Process payload
+ "500":
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ description: Error message
+ examples:
+ Error:
+ value:
+ message: Something unexpected happened
+ description: Error payload
+ operationId: Process
+ summary: Process incoming payload
+components:
+ schemas:
+ Payload:
+ title: Payload to be processed
+ description: Payload to be processed following the Payload type's schema.
+ type: object
+ properties:
+ id:
+ description: Payload Id
+ type: string
+ content:
+ description: Payload Content
+ type: string
+ processed:
+ description: Is the Payload processed
+ type: boolean
+ required:
+ - id
+ - content
+ additionalProperties: false
+tags:
+ - name: payload
+ description: Payload resource