diff --git a/.bash_aliases b/.bash_aliases
index 71caf56..1f971c0 100644
--- a/.bash_aliases
+++ b/.bash_aliases
@@ -20,5 +20,8 @@ alias ex3s="cd ${GITPOD_REPO_ROOT}/exercises/async-activity-completion/solution"
alias ex3w="mvn exec:java -Dexec.mainClass='asyncactivitycompletion.AsyncActivityCompletionWorker'"
alias ex3st="mvn exec:java -Dexec.mainClass='asyncactivitycompletion.Starter'"
alias ex3sg="mvn exec:java -Dexec.mainClass='asyncactivitycompletion.SignalClient'"
+ex3c() {
+ mvn exec:java -Dexec.mainClass="asyncactivitycompletion.CompletionClient" -Dexec.args="${1}"
+}
echo "Your workspace is located at: ${GITPOD_REPO_ROOT}"
echo "Type the command workspace to return to the workspace directory at any time."
diff --git a/.gitignore b/.gitignore
index 524f096..80e6b9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Compiled class file
*.class
+**target*
# Log file
*.log
diff --git a/exercises/async-activity-completion/README.md b/exercises/async-activity-completion/README.md
new file mode 100644
index 0000000..340df47
--- /dev/null
+++ b/exercises/async-activity-completion/README.md
@@ -0,0 +1,90 @@
+# Exercise 4: Asynchronous Activity Completion
+
+During this exercise, you will:
+
+- Retrieve a task token from your Activity execution
+- Set the `doNotCompleteOnReturn()` context to indicate that the Activity is waiting for an external completion.
+- Use another Temporal Client to communicate the result of the asynchronous Activity back to the Workflow
+
+Make your changes to the code in the `practice` subdirectory (look for `TODO` comments that will guide you to where you should make changes to the code). If you need a hint or want to verify your changes, look at the complete version in the `solution` subdirectory.
+
+### GitPod Environment Shortcuts
+
+If you are executing the exercises in the provided GitPod environment, you
+can take advantage of certain aliases to aid in navigation and execution of
+the code.
+
+| Command | Action |
+| :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `ex4` | Change to Exercise 3 Practice Directory |
+| `ex4s` | Change to Exercise 3 Solution Directory |
+| `ex4w` | Execute the Exercise 3 Worker. Must be within the appropriate directory for this to succeed. (either `practice` or `solution`) |
+| `ex4st` | Execute the Exercise 3 Starter. Must be within the appropriate directory for this to succeed. (either `practice` or `solution`) |
+| `ex4c TASK_TOKEN TRANSLATION` | Complete the Exercise 3 Activity, passing in the Task Token and verified translation. Must be within the appropriate directory for this to succeed. (either `practice` or `solution`) |
+
+## Part A: Retrieving the Task Token
+
+1. Open the `TranslationActivitiesActivitiesImpl.java` file in the `src/main/java/asyncactivitycompletion` subdirectory.
+1. In the `translateTerm()` method, add the line `ActivityExecutionContext context = Activity.getExecutionContext();` to get the current Execution Context of the Activity.
+1. Add a call to `getTaskToken()` from the `context` object above and store it in a `byte []` named `taskToken`
+1. Uncomment the line below to convert the `taskToken` byte array to Base64.
+1. Log the Task Token at `info` level using the `logger` object for later use. You will need to convert this to a new String.
+1. Save the file.
+
+## Part B: Set Your Activity to `doNotCompleteOnReturn()`
+
+1. Continue editing the same Activity definition in the `TranslationActivitiesImpl.java` file.
+ 1. Add a call to the `doNotCompleteOnReturn();` method at the end of the `translateTerm()` method using the `context` object from Part A. This notifies Temporal that the Activity should not be completed on return and will be completed asynchronously.
+ 1. Save the file.
+1. Open the `TranslationWorkflowImpl.java` file in the `src/main/java/asyncactivitycompletion` subdirectory.
+ 1. Observe that the Workflow's `StartToCloseTimeout` has been lengthened to `300` seconds for this exercise. Activities can still time out if they are running in the background.
+ 1. If you don't do this and your Activity retries due to a timeout, the Task Token will be reset.
+ 1. Close this file without making any changes.
+
+**Note:** In practice, it is recommended to use Heartbeats for longer running
+Activities. While this exercise doesn't include them, it is a good idea to
+include them in Activities that will complete Asynchronously.
+
+## Part C: Configure a Client to send CompleteActivity
+
+1. Open the `VerifyAndCompleteTranslation.java` file in the `src/main/java/asyncactivitycompletion` subdirectory.
+1. The first thing you'll need to do is add some way of supplying the `taskToken` and translated text specific to the Activity you are trying to complete at runtime. In a production system, you might store and retrieve the token from a database, but for now, you can configure this Client to accept it as a command line argument. Both the `taskToken` and `translation` can be found in the logs of the Worker.
+ 1. Read in the token from the command line `args[0]` and decode the base 64, storing it in a `byte[]`. Hint, invert the call in `TranslationActivitiesImpl.java`
+ 1. Read in the Translation of the phrase that was outputted in the Worker logs as `args[1]`
+1. Add a call to the `complete();` method using the `activityCompletionClient`. This call should provide the task token and result of the Activity. This notifies Temporal that the Activity should not be completed on return and will be completed asynchronously.
+ 1. The result has already been instantiated into a `TranslationActivityOutput` object for you.
+1. Save the file.
+
+## Part D: Running the Workflow and Completing it Asynchronously
+
+At this point, you can run your Workflow. As with the Signal Exercise, the Workflow will not return on its own -- in this case, because your Activity is set to complete asynchronously, and will wait to receive `complete()`.
+
+1. In one terminal, navigate to the `practice` subdirectory
+ 1. If you're in the GitPod environment you can instead run `ex4`
+ 1. Compile the code using `mvn clean compile`
+ 1. Run the worker using `mvn exec:java -Dexec.mainClas='asyncactivitycompletion.TranslationWorker'`.
+ 1. If you're in the GitPod environment you can instead run `ex4w`
+1. In another terminal, navigate to the `practice` subdirectory
+ 1. If you're in the GitPod environment you can instead run `ex4`
+ 1. Invoke the Workflow using `mvn exec:java -Dexec.mainClass='asyncactivitycompletion.Starter' -Dexec.args="Mason de"`
+ If you're in the GitPod environment you can instead run `ex4st Mason de`, replacing the name with yours
+1. Navigate back to the Worker terminal. Your work will produce some logging, eventually including your `taskToken`:
+
+```
+10:28:40.579 INFO - sayHelloGoodbye Workflow Invoked with input name: Mason language code: de
+10:28:40.614 INFO - translateTerm Activity received input: asyncactivitycompletion.model.TranslationActivityInput@394250e6
+10:28:40.614 INFO - TASK TOKEN: CiQ1NzVkNTNlYi1lM2UyLTRmNmEtODFjMy04ZmY0NmJiYjJjOWYSFHRyYW5zbGF0aW9uLXdvcmtmbG93GiQ0OWQ5NjgyOC1iYmJkLTQ5MjMtOTE4Mi00MWY2YmFlNjI4YzEgBSgBMiRlNGJmZmJhMC1jNGJhLTM1MDgtYThkYS01MjgwYjNjMzVkZmJCDVRyYW5zbGF0ZVRlcm1KCAgBEJuAQBgB
+10:28:40.614 INFO - [ACTIVITY INVOKED] translateTerm invoked with input term: hello language code: de
+10:28:40.642 INFO - Translation Service returned: Hallo
+```
+
+1. You can now use this token to send a `complete()` call from another client. In another terminal, navigate to the `practice` subdirectory
+ 1. If you're in the GitPod environment you can instead run `ex4`
+ 1. Run the command `mvn exec:java -Dexec.mainClass='asyncactivitycompletion.VerifyAndCompleteTranslatin' -Dexec.args="TASK_TOKEN TRANSLATION"` with your Task Token replacing `TASK_TOKEN` with your Task Token to complete the Activity and replacing `TRANSLATION` with the results from the translation service
+ 1. If you're in the GitPod environment you can instead run `ex4c TASK_TOKEN TRANSLATION` replacing `TASK_TOKEN` with your Task Token to complete the Activity and replacing `TRANSLATION` with the results from the translation service
+ 1. This will cause your Activity to return and your Workflow to successfully complete. The terminal running your Worker process should now show
+ ```
+ 12:07:43.689 INFO - Workflow Completed
+ ```
+
+### This is the end of the exercise.
diff --git a/exercises/async-activity-completion/practice/pom.xml b/exercises/async-activity-completion/practice/pom.xml
new file mode 100644
index 0000000..52698ac
--- /dev/null
+++ b/exercises/async-activity-completion/practice/pom.xml
@@ -0,0 +1,109 @@
+
+
+
+ 4.0.0
+
+ io.temporal.learn
+ async-activity-completion
+ 1.0.0-SNAPSHOT
+
+ async activity completion
+ https://learn.temporal.io/
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ io.temporal
+ temporal-sdk
+ 1.20.1
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.8
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.11
+
+
+
+ io.temporal
+ temporal-testing
+ 1.20.1
+ test
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.5.2
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.3.1
+ test
+
+
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.1.0
+
+
+
+ maven-resources-plugin
+ 3.0.2
+
+
+ maven-compiler-plugin
+ 3.8.0
+
+
+ maven-surefire-plugin
+ 2.22.1
+
+
+ maven-jar-plugin
+ 3.0.2
+
+
+ maven-install-plugin
+ 2.5.2
+
+
+ maven-deploy-plugin
+ 2.8.2
+
+
+
+ maven-site-plugin
+ 3.7.1
+
+
+ maven-project-info-reports-plugin
+ 3.0.0
+
+
+
+
+
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/Starter.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/Starter.java
new file mode 100644
index 0000000..af2f975
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/Starter.java
@@ -0,0 +1,33 @@
+package asyncactivitycompletion;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import asyncactivitycompletion.model.TranslationWorkflowInput;
+import asyncactivitycompletion.model.TranslationWorkflowOutput;
+
+public class Starter {
+ public static void main(String[] args) throws Exception {
+
+ WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+
+ WorkflowClient client = WorkflowClient.newInstance(service);
+
+ WorkflowOptions options = WorkflowOptions.newBuilder()
+ .setWorkflowId("translation-workflow")
+ .setTaskQueue("translation-tasks")
+ .build();
+
+ TranslationWorkflow workflow = client.newWorkflowStub(TranslationWorkflow.class, options);
+
+ String name = args[0];
+ String languageCode = args[1];
+
+ TranslationWorkflowInput input = new TranslationWorkflowInput(name, languageCode);
+
+ TranslationWorkflowOutput greeting = workflow.sayHelloGoodbye(input);
+
+ System.out.printf("Workflow result: %s\n", greeting);
+ System.exit(0);
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationActivities.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationActivities.java
new file mode 100644
index 0000000..8b4b3f6
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationActivities.java
@@ -0,0 +1,12 @@
+package asyncactivitycompletion;
+
+import io.temporal.activity.ActivityInterface;
+import asyncactivitycompletion.model.TranslationActivityInput;
+import asyncactivitycompletion.model.TranslationActivityOutput;
+
+@ActivityInterface
+public interface TranslationActivities {
+
+ TranslationActivityOutput translateTerm(TranslationActivityInput input);
+
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationActivitiesImpl.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationActivitiesImpl.java
new file mode 100644
index 0000000..b1378bc
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationActivitiesImpl.java
@@ -0,0 +1,127 @@
+package asyncactivitycompletion;
+
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Base64;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import io.temporal.activity.Activity;
+import io.temporal.failure.ApplicationFailure;
+import java.net.HttpURLConnection;
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.client.ActivityCompletionClient;
+import io.temporal.client.WorkflowClient;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Base64;
+import asyncactivitycompletion.model.TranslationActivityInput;
+import asyncactivitycompletion.model.TranslationActivityOutput;
+
+public class TranslationActivitiesImpl implements TranslationActivities {
+
+ private static final Logger logger = LoggerFactory.getLogger(TranslationActivitiesImpl.class);
+
+ @Override
+ public TranslationActivityOutput translateTerm(TranslationActivityInput input) {
+
+ logger.info("translateTerm Activity received input: {}", input);
+
+ // TODO PART A: Add the call to `getExecutionContext()`
+
+ // TODO: PART A: Get the task token using the context object defined in the previous step.
+ // The taskToken should be a byte[]
+
+ // TODO: PART A: Uncomment me
+ //String encoded = new String(Base64.getEncoder().encode(taskToken));
+
+ // TODO: PART A: Log the encoded task token
+
+ String term = input.getTerm();
+ String lang = input.getLanguageCode();
+
+ logger.info("[ACTIVITY INVOKED] translateTerm invoked with input term: {} language code: {}",
+ term, lang);
+
+ // construct the URL, with supplied input parameters, for accessing the
+ // microservice
+ URL url = null;
+ try {
+ String baseUrl = "http://localhost:9999/translate?term=%s&lang=%s";
+ url = URI.create(
+ String.format(baseUrl,
+ URLEncoder.encode(term, "UTF-8"),
+ URLEncoder.encode(lang, "UTF-8")))
+ .toURL();
+ } catch (IOException e) {
+ logger.error(e.getMessage());
+ throw Activity.wrap(e);
+ }
+
+ TranslationActivityOutput result = new TranslationActivityOutput();
+
+ try {
+ // Open a connection to the URL
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+ // Set the HTTP request method (GET, POST, etc.)
+ connection.setRequestMethod("GET");
+
+ // Get the response code
+ int responseCode = connection.getResponseCode();
+
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ // If the response code is 200 (HTTP OK), the request was successful
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(connection.getInputStream()));
+ String line;
+ StringBuilder response = new StringBuilder();
+
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+
+ reader.close();
+
+ connection.disconnect();
+ result.setTranslation(response.toString());
+
+ } else {
+ // If the response code is not 200, there was an error
+ BufferedReader errorReader = new BufferedReader(
+ new InputStreamReader(connection.getErrorStream()));
+ String line;
+ StringBuilder errorResponse = new StringBuilder();
+
+ while ((line = errorReader.readLine()) != null) {
+ errorResponse.append(line);
+ }
+
+ errorReader.close();
+
+ connection.disconnect();
+ // Print the error response
+ throw ApplicationFailure.newFailure(errorResponse.toString(), IOException.class.getName());
+ }
+
+ } catch (IOException e) {
+ logger.error(e.getMessage());
+ throw Activity.wrap(e);
+ }
+
+ logger.info("Translation Service returned: {}", result.getTranslation());
+
+ // TODO: PART B: Use the `context` object from above to call `doNotCompleteOnReturn()`;
+ // This notifies Temporal that the Activity should not be completed on return and will be completed asynchronously.
+
+
+ // Since we have set doNotCompleteOnReturn(), the return value is ignored.
+ return new TranslationActivityOutput("this will be ignored");
+ }
+
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorker.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorker.java
new file mode 100644
index 0000000..44a1e4b
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorker.java
@@ -0,0 +1,23 @@
+package asyncactivitycompletion;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+
+public class TranslationWorker {
+ public static void main(String[] args) {
+
+ WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker("translation-tasks");
+
+ worker.registerWorkflowImplementationTypes(TranslationWorkflowImpl.class);
+
+ worker.registerActivitiesImplementations(new TranslationActivitiesImpl());
+
+ factory.start();
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorkflow.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorkflow.java
new file mode 100644
index 0000000..130133e
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorkflow.java
@@ -0,0 +1,14 @@
+package asyncactivitycompletion;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import asyncactivitycompletion.model.TranslationWorkflowInput;
+import asyncactivitycompletion.model.TranslationWorkflowOutput;
+
+@WorkflowInterface
+public interface TranslationWorkflow {
+
+ @WorkflowMethod
+ TranslationWorkflowOutput sayHelloGoodbye(TranslationWorkflowInput input);
+
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorkflowImpl.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorkflowImpl.java
new file mode 100644
index 0000000..73e007b
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/TranslationWorkflowImpl.java
@@ -0,0 +1,42 @@
+package asyncactivitycompletion;
+
+import java.time.Duration;
+import org.slf4j.Logger;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import asyncactivitycompletion.model.TranslationActivityInput;
+import asyncactivitycompletion.model.TranslationActivityOutput;
+import asyncactivitycompletion.model.TranslationWorkflowInput;
+import asyncactivitycompletion.model.TranslationWorkflowOutput;
+
+public class TranslationWorkflowImpl implements TranslationWorkflow {
+
+ public static final Logger logger = Workflow.getLogger(TranslationWorkflowImpl.class);
+
+ private final ActivityOptions options =
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(300))
+ .build();
+
+ private final TranslationActivities activities =
+ Workflow.newActivityStub(TranslationActivities.class, options);
+
+ @Override
+ public TranslationWorkflowOutput sayHelloGoodbye(TranslationWorkflowInput input) {
+ String name = input.getName();
+ String languageCode = input.getLanguageCode();
+
+ logger.info("sayHelloGoodbye Workflow Invoked with input name: {} language code: {}", name,
+ languageCode);
+
+ logger.debug("Preparing to translate Hello into languageCode: {}", languageCode);
+ TranslationActivityInput helloInput = new TranslationActivityInput("hello", languageCode);
+ TranslationActivityOutput helloResult = activities.translateTerm(helloInput);
+ String helloMessage = helloResult.getTranslation() + ", " + name;
+
+ logger.info("Workflow completed");
+
+ return new TranslationWorkflowOutput(helloMessage);
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/VerifyAndCompleteTranslation.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/VerifyAndCompleteTranslation.java
new file mode 100644
index 0000000..b6e9871
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/VerifyAndCompleteTranslation.java
@@ -0,0 +1,38 @@
+package asyncactivitycompletion;
+
+import java.util.Base64;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.client.ActivityCompletionClient;
+
+import asyncactivitycompletion.model.TranslationActivityOutput;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+public class VerifyAndCompleteTranslation {
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+
+ WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+
+ WorkflowClient client = WorkflowClient.newInstance(service);
+
+
+ // TODO: PART C: Read in the taskToken from args[0] and decode it from Base64
+ byte[] taskToken = Base64.getDecoder().decode(args[0]);
+
+ // TODO: PART C: Get the translated text from args[1]
+
+ ActivityCompletionClient activityCompletionClient = client.newActivityCompletionClient();
+
+ TranslationActivityOutput result = new TranslationActivityOutput(result);
+
+ // TODO: PART C: Call the `complete()` method using the `activityCompletionClient` object
+ // from above. The call should contain the `taskToken` and the `result`.
+
+ System.exit(0);
+ }
+}
\ No newline at end of file
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationActivityInput.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationActivityInput.java
new file mode 100644
index 0000000..4b87e58
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationActivityInput.java
@@ -0,0 +1,59 @@
+package asyncactivitycompletion.model;
+
+import java.util.Objects;
+
+public class TranslationActivityInput {
+
+ private String term;
+ private String languageCode;
+
+ public TranslationActivityInput() {
+ }
+
+ public TranslationActivityInput(String term, String languageCode) {
+ this.term = term;
+ this.languageCode = languageCode;
+ }
+
+ public String getTerm() {
+ return term;
+ }
+
+ public void setTerm(String term) {
+ this.term = term;
+ }
+
+ public String getLanguageCode() {
+ return languageCode;
+ }
+
+ public void setLanguageCode(String languageCode) {
+ this.languageCode = languageCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final TranslationActivityInput other = (TranslationActivityInput) obj;
+ if (!Objects.equals(this.term, other.term)) {
+ return false;
+ }
+ return Objects.equals(this.languageCode, other.languageCode);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 3;
+ hash = 53 * hash + Objects.hashCode(this.term);
+ hash = 53 * hash + Objects.hashCode(this.languageCode);
+ return hash;
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationActivityOutput.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationActivityOutput.java
new file mode 100644
index 0000000..71c6bc4
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationActivityOutput.java
@@ -0,0 +1,21 @@
+package asyncactivitycompletion.model;
+
+public class TranslationActivityOutput {
+
+ private String translation;
+
+ public TranslationActivityOutput() {
+ }
+
+ public TranslationActivityOutput(String translation) {
+ this.translation = translation;
+ }
+
+ public String getTranslation() {
+ return translation;
+ }
+
+ public void setTranslation(String translation) {
+ this.translation = translation;
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationWorkflowInput.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationWorkflowInput.java
new file mode 100644
index 0000000..0737015
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationWorkflowInput.java
@@ -0,0 +1,31 @@
+package asyncactivitycompletion.model;
+
+public class TranslationWorkflowInput {
+
+ private String name;
+ private String languageCode;
+
+ public TranslationWorkflowInput() {
+ }
+
+ public TranslationWorkflowInput(String name, String languageCode) {
+ this.name = name;
+ this.languageCode = languageCode;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLanguageCode() {
+ return languageCode;
+ }
+
+ public void setLanguageCode(String languageCode) {
+ this.languageCode = languageCode;
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationWorkflowOutput.java b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationWorkflowOutput.java
new file mode 100644
index 0000000..e12c2d8
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/java/asyncactivitycompletion/model/TranslationWorkflowOutput.java
@@ -0,0 +1,26 @@
+package asyncactivitycompletion.model;
+
+public class TranslationWorkflowOutput {
+
+ private String helloMessage;
+
+ public TranslationWorkflowOutput() {
+ }
+
+ public TranslationWorkflowOutput(String helloMessage) {
+ this.helloMessage = helloMessage;
+ }
+
+ public String getHelloMessage() {
+ return helloMessage;
+ }
+
+ public void setHelloMessage(String helloMessage) {
+ this.helloMessage = helloMessage;
+ }
+
+ @Override
+ public String toString() {
+ return this.helloMessage;
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/main/resources/logback.xml b/exercises/async-activity-completion/practice/src/main/resources/logback.xml
new file mode 100644
index 0000000..8e488b0
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/main/resources/logback.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level - %msg %n
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationActivitiesTest.java b/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationActivitiesTest.java
new file mode 100644
index 0000000..10dfa8c
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationActivitiesTest.java
@@ -0,0 +1,61 @@
+package translationworkflow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.temporal.failure.ActivityFailure;
+import io.temporal.testing.TestActivityEnvironment;
+import translationworkflow.model.TranslationActivityInput;
+import translationworkflow.model.TranslationActivityOutput;
+
+public class TranslationActivitiesTest {
+
+ private TestActivityEnvironment testEnvironment;
+ private TranslationActivities activity;
+
+ @BeforeEach
+ public void init() {
+ testEnvironment = TestActivityEnvironment.newInstance();
+ testEnvironment.registerActivitiesImplementations(new TranslationActivitiesImpl());
+ activity = testEnvironment.newActivityStub(TranslationActivities.class);
+ }
+
+ @AfterEach
+ public void destroy() {
+ testEnvironment.close();
+ }
+
+ @Test
+ public void testSuccessfulTranslateActivityHelloGerman() {
+ TranslationActivityInput input = new TranslationActivityInput("hello", "de");
+ TranslationActivityOutput output = activity.translateTerm(input);
+ assertEquals("Hallo", output.getTranslation());
+ }
+
+ @Test
+ public void testSuccessfulTranslateActivityHelloLatvian() {
+ TranslationActivityInput input = new TranslationActivityInput("goodbye", "lv");
+ TranslationActivityOutput output = activity.translateTerm(input);
+ assertEquals("Ardievu", output.getTranslation());
+ }
+
+ @Test
+ public void testFailedTranslateActivityBadLanguageCode() {
+ TranslationActivityInput input = new TranslationActivityInput("goodbye", "xq");
+
+ // Assert that an error was thrown and it was an Activity Failure
+ Exception exception = assertThrows(ActivityFailure.class, () -> {
+ TranslationActivityOutput output = activity.translateTerm(input);
+ });
+
+ // Assert that the error contains the expected message
+ assertTrue(exception.getMessage().contains(
+ "Invalid language code"),
+ "expected error message");
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationWorkflowMockTest.java b/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationWorkflowMockTest.java
new file mode 100644
index 0000000..8f6215e
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationWorkflowMockTest.java
@@ -0,0 +1,46 @@
+package translationworkflow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.temporal.testing.TestWorkflowEnvironment;
+import io.temporal.testing.TestWorkflowExtension;
+import io.temporal.worker.Worker;
+import translationworkflow.model.TranslationActivityInput;
+import translationworkflow.model.TranslationActivityOutput;
+import translationworkflow.model.TranslationWorkflowInput;
+import translationworkflow.model.TranslationWorkflowOutput;
+import static org.mockito.Mockito.*;
+
+public class TranslationWorkflowMockTest {
+
+ @RegisterExtension
+ public static final TestWorkflowExtension testWorkflowExtension =
+ TestWorkflowExtension.newBuilder()
+ .setWorkflowTypes(TranslationWorkflowImpl.class)
+ .setDoNotStart(true)
+ .build();
+
+ @Test
+ public void testSuccessfulTranslationWithMocks(TestWorkflowEnvironment testEnv, Worker worker,
+ TranslationWorkflow workflow) {
+
+ TranslationActivities mockedActivities =
+ mock(TranslationActivities.class, withSettings().withoutAnnotations());
+ when(mockedActivities.translateTerm(new TranslationActivityInput("hello", "fr")))
+ .thenReturn(new TranslationActivityOutput("Bonjour"));
+ when(mockedActivities.translateTerm(new TranslationActivityInput("goodbye", "fr")))
+ .thenReturn(new TranslationActivityOutput("Au revoir"));
+
+ worker.registerActivitiesImplementations(mockedActivities);
+ testEnv.start();
+
+ TranslationWorkflowOutput output =
+ workflow.sayHelloGoodbye(new TranslationWorkflowInput("Pierre", "fr"));
+
+ assertEquals("Bonjour, Pierre", output.getHelloMessage());
+ assertEquals("Au revoir, Pierre", output.getGoodbyeMessage());
+ }
+}
diff --git a/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationWorkflowTest.java b/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationWorkflowTest.java
new file mode 100644
index 0000000..4516bca
--- /dev/null
+++ b/exercises/async-activity-completion/practice/src/test/java/translationworkflow/TranslationWorkflowTest.java
@@ -0,0 +1,38 @@
+package translationworkflow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.temporal.testing.TestWorkflowEnvironment;
+import io.temporal.testing.TestWorkflowExtension;
+import io.temporal.worker.Worker;
+import translationworkflow.model.TranslationActivityInput;
+import translationworkflow.model.TranslationActivityOutput;
+import translationworkflow.model.TranslationWorkflowInput;
+import translationworkflow.model.TranslationWorkflowOutput;
+
+public class TranslationWorkflowTest {
+
+ @RegisterExtension
+ public static final TestWorkflowExtension testWorkflowExtension =
+ TestWorkflowExtension.newBuilder()
+ .setWorkflowTypes(TranslationWorkflowImpl.class)
+ .setDoNotStart(true)
+ .build();
+
+ @Test
+ public void testSuccessfulTranslation(TestWorkflowEnvironment testEnv, Worker worker,
+ TranslationWorkflow workflow) {
+
+ worker.registerActivitiesImplementations(new TranslationActivitiesImpl());
+ testEnv.start();
+
+ TranslationWorkflowOutput output =
+ workflow.sayHelloGoodbye(new TranslationWorkflowInput("Pierre", "fr"));
+
+ assertEquals("Bonjour, Pierre", output.getHelloMessage());
+ assertEquals("Au revoir, Pierre", output.getGoodbyeMessage());
+ }
+}
diff --git a/exercises/async-activity-completion/solution/pom.xml b/exercises/async-activity-completion/solution/pom.xml
new file mode 100644
index 0000000..3e360ad
--- /dev/null
+++ b/exercises/async-activity-completion/solution/pom.xml
@@ -0,0 +1,109 @@
+
+
+
+ 4.0.0
+
+ io.temporal.learn
+ async-activity-completion-solution
+ 1.0.0-SNAPSHOT
+
+ async activity completion (solution)
+ https://learn.temporal.io/
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ io.temporal
+ temporal-sdk
+ 1.20.1
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.8
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.11
+
+
+
+ io.temporal
+ temporal-testing
+ 1.20.1
+ test
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.5.2
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.3.1
+ test
+
+
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.1.0
+
+
+
+ maven-resources-plugin
+ 3.0.2
+
+
+ maven-compiler-plugin
+ 3.8.0
+
+
+ maven-surefire-plugin
+ 2.22.1
+
+
+ maven-jar-plugin
+ 3.0.2
+
+
+ maven-install-plugin
+ 2.5.2
+
+
+ maven-deploy-plugin
+ 2.8.2
+
+
+
+ maven-site-plugin
+ 3.7.1
+
+
+ maven-project-info-reports-plugin
+ 3.0.0
+
+
+
+
+
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/Starter.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/Starter.java
new file mode 100644
index 0000000..af2f975
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/Starter.java
@@ -0,0 +1,33 @@
+package asyncactivitycompletion;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.client.WorkflowOptions;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import asyncactivitycompletion.model.TranslationWorkflowInput;
+import asyncactivitycompletion.model.TranslationWorkflowOutput;
+
+public class Starter {
+ public static void main(String[] args) throws Exception {
+
+ WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+
+ WorkflowClient client = WorkflowClient.newInstance(service);
+
+ WorkflowOptions options = WorkflowOptions.newBuilder()
+ .setWorkflowId("translation-workflow")
+ .setTaskQueue("translation-tasks")
+ .build();
+
+ TranslationWorkflow workflow = client.newWorkflowStub(TranslationWorkflow.class, options);
+
+ String name = args[0];
+ String languageCode = args[1];
+
+ TranslationWorkflowInput input = new TranslationWorkflowInput(name, languageCode);
+
+ TranslationWorkflowOutput greeting = workflow.sayHelloGoodbye(input);
+
+ System.out.printf("Workflow result: %s\n", greeting);
+ System.exit(0);
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationActivities.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationActivities.java
new file mode 100644
index 0000000..8b4b3f6
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationActivities.java
@@ -0,0 +1,12 @@
+package asyncactivitycompletion;
+
+import io.temporal.activity.ActivityInterface;
+import asyncactivitycompletion.model.TranslationActivityInput;
+import asyncactivitycompletion.model.TranslationActivityOutput;
+
+@ActivityInterface
+public interface TranslationActivities {
+
+ TranslationActivityOutput translateTerm(TranslationActivityInput input);
+
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationActivitiesImpl.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationActivitiesImpl.java
new file mode 100644
index 0000000..c4dc0e0
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationActivitiesImpl.java
@@ -0,0 +1,125 @@
+package asyncactivitycompletion;
+
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Base64;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import io.temporal.activity.Activity;
+import io.temporal.failure.ApplicationFailure;
+import java.net.HttpURLConnection;
+import io.temporal.activity.ActivityExecutionContext;
+import io.temporal.client.ActivityCompletionClient;
+import io.temporal.client.WorkflowClient;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Base64;
+import asyncactivitycompletion.model.TranslationActivityInput;
+import asyncactivitycompletion.model.TranslationActivityOutput;
+
+public class TranslationActivitiesImpl implements TranslationActivities {
+
+ private static final Logger logger = LoggerFactory.getLogger(TranslationActivitiesImpl.class);
+
+ @Override
+ public TranslationActivityOutput translateTerm(TranslationActivityInput input) {
+
+ logger.info("translateTerm Activity received input: {}", input);
+
+ // Get the activity execution context
+ ActivityExecutionContext context = Activity.getExecutionContext();
+
+ // Set a correlation token that can be used to complete the activity asynchronously
+ byte[] taskToken = context.getTaskToken();
+
+ String encoded = new String(Base64.getEncoder().encode(taskToken));
+
+ logger.info("TASK TOKEN: {}", encoded);
+
+ String term = input.getTerm();
+ String lang = input.getLanguageCode();
+
+ logger.info("[ACTIVITY INVOKED] translateTerm invoked with input term: {} language code: {}",
+ term, lang);
+
+ // construct the URL, with supplied input parameters, for accessing the
+ // microservice
+ URL url = null;
+ try {
+ String baseUrl = "http://localhost:9999/translate?term=%s&lang=%s";
+ url = URI.create(
+ String.format(baseUrl,
+ URLEncoder.encode(term, "UTF-8"),
+ URLEncoder.encode(lang, "UTF-8")))
+ .toURL();
+ } catch (IOException e) {
+ logger.error(e.getMessage());
+ throw Activity.wrap(e);
+ }
+
+ TranslationActivityOutput result = new TranslationActivityOutput();
+
+ try {
+ // Open a connection to the URL
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+ // Set the HTTP request method (GET, POST, etc.)
+ connection.setRequestMethod("GET");
+
+ // Get the response code
+ int responseCode = connection.getResponseCode();
+
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ // If the response code is 200 (HTTP OK), the request was successful
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(connection.getInputStream()));
+ String line;
+ StringBuilder response = new StringBuilder();
+
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+
+ reader.close();
+
+ connection.disconnect();
+ result.setTranslation(response.toString());
+
+ } else {
+ // If the response code is not 200, there was an error
+ BufferedReader errorReader = new BufferedReader(
+ new InputStreamReader(connection.getErrorStream()));
+ String line;
+ StringBuilder errorResponse = new StringBuilder();
+
+ while ((line = errorReader.readLine()) != null) {
+ errorResponse.append(line);
+ }
+
+ errorReader.close();
+
+ connection.disconnect();
+ // Print the error response
+ throw ApplicationFailure.newFailure(errorResponse.toString(), IOException.class.getName());
+ }
+
+ } catch (IOException e) {
+ logger.error(e.getMessage());
+ throw Activity.wrap(e);
+ }
+
+ logger.info("Translation Service returned: {}", result.getTranslation());
+
+ context.doNotCompleteOnReturn();
+
+ // Since we have set doNotCompleteOnReturn(), the return value is ignored.
+ return new TranslationActivityOutput("this will be ignored");
+ }
+
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorker.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorker.java
new file mode 100644
index 0000000..44a1e4b
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorker.java
@@ -0,0 +1,23 @@
+package asyncactivitycompletion;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.worker.Worker;
+import io.temporal.worker.WorkerFactory;
+
+public class TranslationWorker {
+ public static void main(String[] args) {
+
+ WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+ WorkflowClient client = WorkflowClient.newInstance(service);
+ WorkerFactory factory = WorkerFactory.newInstance(client);
+
+ Worker worker = factory.newWorker("translation-tasks");
+
+ worker.registerWorkflowImplementationTypes(TranslationWorkflowImpl.class);
+
+ worker.registerActivitiesImplementations(new TranslationActivitiesImpl());
+
+ factory.start();
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorkflow.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorkflow.java
new file mode 100644
index 0000000..130133e
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorkflow.java
@@ -0,0 +1,14 @@
+package asyncactivitycompletion;
+
+import io.temporal.workflow.WorkflowInterface;
+import io.temporal.workflow.WorkflowMethod;
+import asyncactivitycompletion.model.TranslationWorkflowInput;
+import asyncactivitycompletion.model.TranslationWorkflowOutput;
+
+@WorkflowInterface
+public interface TranslationWorkflow {
+
+ @WorkflowMethod
+ TranslationWorkflowOutput sayHelloGoodbye(TranslationWorkflowInput input);
+
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorkflowImpl.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorkflowImpl.java
new file mode 100644
index 0000000..73e007b
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/TranslationWorkflowImpl.java
@@ -0,0 +1,42 @@
+package asyncactivitycompletion;
+
+import java.time.Duration;
+import org.slf4j.Logger;
+
+import io.temporal.activity.ActivityOptions;
+import io.temporal.workflow.Workflow;
+import asyncactivitycompletion.model.TranslationActivityInput;
+import asyncactivitycompletion.model.TranslationActivityOutput;
+import asyncactivitycompletion.model.TranslationWorkflowInput;
+import asyncactivitycompletion.model.TranslationWorkflowOutput;
+
+public class TranslationWorkflowImpl implements TranslationWorkflow {
+
+ public static final Logger logger = Workflow.getLogger(TranslationWorkflowImpl.class);
+
+ private final ActivityOptions options =
+ ActivityOptions.newBuilder()
+ .setStartToCloseTimeout(Duration.ofSeconds(300))
+ .build();
+
+ private final TranslationActivities activities =
+ Workflow.newActivityStub(TranslationActivities.class, options);
+
+ @Override
+ public TranslationWorkflowOutput sayHelloGoodbye(TranslationWorkflowInput input) {
+ String name = input.getName();
+ String languageCode = input.getLanguageCode();
+
+ logger.info("sayHelloGoodbye Workflow Invoked with input name: {} language code: {}", name,
+ languageCode);
+
+ logger.debug("Preparing to translate Hello into languageCode: {}", languageCode);
+ TranslationActivityInput helloInput = new TranslationActivityInput("hello", languageCode);
+ TranslationActivityOutput helloResult = activities.translateTerm(helloInput);
+ String helloMessage = helloResult.getTranslation() + ", " + name;
+
+ logger.info("Workflow completed");
+
+ return new TranslationWorkflowOutput(helloMessage);
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/VerifyAndCompleteTranslation.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/VerifyAndCompleteTranslation.java
new file mode 100644
index 0000000..675bef7
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/VerifyAndCompleteTranslation.java
@@ -0,0 +1,36 @@
+package asyncactivitycompletion;
+
+import java.util.Base64;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import io.temporal.client.WorkflowClient;
+import io.temporal.serviceclient.WorkflowServiceStubs;
+import io.temporal.client.ActivityCompletionClient;
+
+import asyncactivitycompletion.model.TranslationActivityOutput;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+public class VerifyAndCompleteTranslation {
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+
+ WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
+
+ WorkflowClient client = WorkflowClient.newInstance(service);
+
+
+ byte[] taskToken = Base64.getDecoder().decode(args[0]);
+
+ // Pass in the translated text as a command line argument and pretend to "verify"
+ // the results
+ String result = args[1];
+
+ ActivityCompletionClient activityCompletionClient = client.newActivityCompletionClient();
+
+ activityCompletionClient.complete(taskToken, new TranslationActivityOutput(result));
+
+ System.exit(0);
+ }
+}
\ No newline at end of file
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationActivityInput.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationActivityInput.java
new file mode 100644
index 0000000..4b87e58
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationActivityInput.java
@@ -0,0 +1,59 @@
+package asyncactivitycompletion.model;
+
+import java.util.Objects;
+
+public class TranslationActivityInput {
+
+ private String term;
+ private String languageCode;
+
+ public TranslationActivityInput() {
+ }
+
+ public TranslationActivityInput(String term, String languageCode) {
+ this.term = term;
+ this.languageCode = languageCode;
+ }
+
+ public String getTerm() {
+ return term;
+ }
+
+ public void setTerm(String term) {
+ this.term = term;
+ }
+
+ public String getLanguageCode() {
+ return languageCode;
+ }
+
+ public void setLanguageCode(String languageCode) {
+ this.languageCode = languageCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final TranslationActivityInput other = (TranslationActivityInput) obj;
+ if (!Objects.equals(this.term, other.term)) {
+ return false;
+ }
+ return Objects.equals(this.languageCode, other.languageCode);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 3;
+ hash = 53 * hash + Objects.hashCode(this.term);
+ hash = 53 * hash + Objects.hashCode(this.languageCode);
+ return hash;
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationActivityOutput.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationActivityOutput.java
new file mode 100644
index 0000000..71c6bc4
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationActivityOutput.java
@@ -0,0 +1,21 @@
+package asyncactivitycompletion.model;
+
+public class TranslationActivityOutput {
+
+ private String translation;
+
+ public TranslationActivityOutput() {
+ }
+
+ public TranslationActivityOutput(String translation) {
+ this.translation = translation;
+ }
+
+ public String getTranslation() {
+ return translation;
+ }
+
+ public void setTranslation(String translation) {
+ this.translation = translation;
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationWorkflowInput.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationWorkflowInput.java
new file mode 100644
index 0000000..0737015
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationWorkflowInput.java
@@ -0,0 +1,31 @@
+package asyncactivitycompletion.model;
+
+public class TranslationWorkflowInput {
+
+ private String name;
+ private String languageCode;
+
+ public TranslationWorkflowInput() {
+ }
+
+ public TranslationWorkflowInput(String name, String languageCode) {
+ this.name = name;
+ this.languageCode = languageCode;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLanguageCode() {
+ return languageCode;
+ }
+
+ public void setLanguageCode(String languageCode) {
+ this.languageCode = languageCode;
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationWorkflowOutput.java b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationWorkflowOutput.java
new file mode 100644
index 0000000..e12c2d8
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/java/asyncactivitycompletion/model/TranslationWorkflowOutput.java
@@ -0,0 +1,26 @@
+package asyncactivitycompletion.model;
+
+public class TranslationWorkflowOutput {
+
+ private String helloMessage;
+
+ public TranslationWorkflowOutput() {
+ }
+
+ public TranslationWorkflowOutput(String helloMessage) {
+ this.helloMessage = helloMessage;
+ }
+
+ public String getHelloMessage() {
+ return helloMessage;
+ }
+
+ public void setHelloMessage(String helloMessage) {
+ this.helloMessage = helloMessage;
+ }
+
+ @Override
+ public String toString() {
+ return this.helloMessage;
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/main/resources/logback.xml b/exercises/async-activity-completion/solution/src/main/resources/logback.xml
new file mode 100644
index 0000000..8e488b0
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/main/resources/logback.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level - %msg %n
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationActivitiesTest.java b/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationActivitiesTest.java
new file mode 100644
index 0000000..10dfa8c
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationActivitiesTest.java
@@ -0,0 +1,61 @@
+package translationworkflow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.temporal.failure.ActivityFailure;
+import io.temporal.testing.TestActivityEnvironment;
+import translationworkflow.model.TranslationActivityInput;
+import translationworkflow.model.TranslationActivityOutput;
+
+public class TranslationActivitiesTest {
+
+ private TestActivityEnvironment testEnvironment;
+ private TranslationActivities activity;
+
+ @BeforeEach
+ public void init() {
+ testEnvironment = TestActivityEnvironment.newInstance();
+ testEnvironment.registerActivitiesImplementations(new TranslationActivitiesImpl());
+ activity = testEnvironment.newActivityStub(TranslationActivities.class);
+ }
+
+ @AfterEach
+ public void destroy() {
+ testEnvironment.close();
+ }
+
+ @Test
+ public void testSuccessfulTranslateActivityHelloGerman() {
+ TranslationActivityInput input = new TranslationActivityInput("hello", "de");
+ TranslationActivityOutput output = activity.translateTerm(input);
+ assertEquals("Hallo", output.getTranslation());
+ }
+
+ @Test
+ public void testSuccessfulTranslateActivityHelloLatvian() {
+ TranslationActivityInput input = new TranslationActivityInput("goodbye", "lv");
+ TranslationActivityOutput output = activity.translateTerm(input);
+ assertEquals("Ardievu", output.getTranslation());
+ }
+
+ @Test
+ public void testFailedTranslateActivityBadLanguageCode() {
+ TranslationActivityInput input = new TranslationActivityInput("goodbye", "xq");
+
+ // Assert that an error was thrown and it was an Activity Failure
+ Exception exception = assertThrows(ActivityFailure.class, () -> {
+ TranslationActivityOutput output = activity.translateTerm(input);
+ });
+
+ // Assert that the error contains the expected message
+ assertTrue(exception.getMessage().contains(
+ "Invalid language code"),
+ "expected error message");
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationWorkflowMockTest.java b/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationWorkflowMockTest.java
new file mode 100644
index 0000000..8f6215e
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationWorkflowMockTest.java
@@ -0,0 +1,46 @@
+package translationworkflow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.temporal.testing.TestWorkflowEnvironment;
+import io.temporal.testing.TestWorkflowExtension;
+import io.temporal.worker.Worker;
+import translationworkflow.model.TranslationActivityInput;
+import translationworkflow.model.TranslationActivityOutput;
+import translationworkflow.model.TranslationWorkflowInput;
+import translationworkflow.model.TranslationWorkflowOutput;
+import static org.mockito.Mockito.*;
+
+public class TranslationWorkflowMockTest {
+
+ @RegisterExtension
+ public static final TestWorkflowExtension testWorkflowExtension =
+ TestWorkflowExtension.newBuilder()
+ .setWorkflowTypes(TranslationWorkflowImpl.class)
+ .setDoNotStart(true)
+ .build();
+
+ @Test
+ public void testSuccessfulTranslationWithMocks(TestWorkflowEnvironment testEnv, Worker worker,
+ TranslationWorkflow workflow) {
+
+ TranslationActivities mockedActivities =
+ mock(TranslationActivities.class, withSettings().withoutAnnotations());
+ when(mockedActivities.translateTerm(new TranslationActivityInput("hello", "fr")))
+ .thenReturn(new TranslationActivityOutput("Bonjour"));
+ when(mockedActivities.translateTerm(new TranslationActivityInput("goodbye", "fr")))
+ .thenReturn(new TranslationActivityOutput("Au revoir"));
+
+ worker.registerActivitiesImplementations(mockedActivities);
+ testEnv.start();
+
+ TranslationWorkflowOutput output =
+ workflow.sayHelloGoodbye(new TranslationWorkflowInput("Pierre", "fr"));
+
+ assertEquals("Bonjour, Pierre", output.getHelloMessage());
+ assertEquals("Au revoir, Pierre", output.getGoodbyeMessage());
+ }
+}
diff --git a/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationWorkflowTest.java b/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationWorkflowTest.java
new file mode 100644
index 0000000..4516bca
--- /dev/null
+++ b/exercises/async-activity-completion/solution/src/test/java/translationworkflow/TranslationWorkflowTest.java
@@ -0,0 +1,38 @@
+package translationworkflow;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.temporal.testing.TestWorkflowEnvironment;
+import io.temporal.testing.TestWorkflowExtension;
+import io.temporal.worker.Worker;
+import translationworkflow.model.TranslationActivityInput;
+import translationworkflow.model.TranslationActivityOutput;
+import translationworkflow.model.TranslationWorkflowInput;
+import translationworkflow.model.TranslationWorkflowOutput;
+
+public class TranslationWorkflowTest {
+
+ @RegisterExtension
+ public static final TestWorkflowExtension testWorkflowExtension =
+ TestWorkflowExtension.newBuilder()
+ .setWorkflowTypes(TranslationWorkflowImpl.class)
+ .setDoNotStart(true)
+ .build();
+
+ @Test
+ public void testSuccessfulTranslation(TestWorkflowEnvironment testEnv, Worker worker,
+ TranslationWorkflow workflow) {
+
+ worker.registerActivitiesImplementations(new TranslationActivitiesImpl());
+ testEnv.start();
+
+ TranslationWorkflowOutput output =
+ workflow.sayHelloGoodbye(new TranslationWorkflowInput("Pierre", "fr"));
+
+ assertEquals("Bonjour, Pierre", output.getHelloMessage());
+ assertEquals("Au revoir, Pierre", output.getGoodbyeMessage());
+ }
+}
diff --git a/utilities/microservice/pom.xml b/utilities/microservice/pom.xml
new file mode 100644
index 0000000..99d8824
--- /dev/null
+++ b/utilities/microservice/pom.xml
@@ -0,0 +1,82 @@
+
+
+
+ 4.0.0
+
+ io.temporal.learn
+ translation-microservice
+ 1.0.0-SNAPSHOT
+
+ translation-microservice
+ https://learn.temporal.io/
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.11
+
+
+
+ org.rapidoid
+ rapidoid-quick
+ 5.5.5
+
+
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.1.0
+
+
+
+ maven-resources-plugin
+ 3.0.2
+
+
+ maven-compiler-plugin
+ 3.8.0
+
+
+ maven-surefire-plugin
+ 2.22.1
+
+
+ maven-jar-plugin
+ 3.0.2
+
+
+ maven-install-plugin
+ 2.5.2
+
+
+ maven-deploy-plugin
+ 2.8.2
+
+
+
+ maven-site-plugin
+ 3.7.1
+
+
+ maven-project-info-reports-plugin
+ 3.0.0
+
+
+
+
+
diff --git a/utilities/microservice/src/main/java/translationapi/Microservice.java b/utilities/microservice/src/main/java/translationapi/Microservice.java
new file mode 100644
index 0000000..bf79014
--- /dev/null
+++ b/utilities/microservice/src/main/java/translationapi/Microservice.java
@@ -0,0 +1,149 @@
+package translationapi;
+
+import java.io.IOException;
+
+import org.rapidoid.http.Req;
+import org.rapidoid.http.ReqRespHandler;
+import org.rapidoid.http.Resp;
+import org.rapidoid.setup.On;
+import org.rapidoid.u.U;
+
+import java.util.Map;
+import java.util.HashMap;
+
+public class Microservice {
+
+ // port number where this service will listen for incoming HTTP requests
+ public static final int PORT_NUMBER = 9999;
+
+ // IP address to which the service will be bound. Using a value of 0.0.0.0
+ // will make it available on all available interfaces, but you could use
+ // 127.0.0.1 to restrict it to the loopback interface
+ public static final String SERVER_IP = "0.0.0.0";
+
+ public static void main(String[] args) throws IOException {
+ // Start the service on the specified IP address and port
+ On.address(SERVER_IP).port(PORT_NUMBER);
+
+ final Map> translations = loadTranslations();
+
+ // Define the service endpoints and handlers
+ On.get("/translate").plain(new TranslationHandler(translations));
+
+ // Also define a catch-all to return an HTTP 404 Not Found error if the URL
+ // path in the request didn't match an endpoint defined above. It's essential
+ // that this code remains at the end.
+ On.req(
+ (req, resp) -> {
+ String message = String.format("Error: Invalid endpoint address '%s'", req.path());
+ return req.response().result(message).code(404);
+ });
+ }
+
+ private static class TranslationHandler implements ReqRespHandler {
+
+ private final Map> translations;
+
+ public TranslationHandler(Map> translations) {
+ super();
+ this.translations = translations;
+ }
+
+ @Override
+ public Object execute(Req req, Resp resp) throws Exception {
+ Map params = req.params();
+
+ if (!params.containsKey("term")) {
+ String message = "Error: Missing required 'term' parameter!";
+ return req.response().result(message).code(500);
+ }
+
+ if (!params.containsKey("lang")) {
+ String message = "Error: Missing required 'lang' parameter!";
+ return req.response().result(message).code(500);
+ }
+
+ String languageCode = params.get("lang");
+ String term = params.get("term");
+
+ if (!translations.containsKey(languageCode)) {
+ String message = "Error: Invalid language code '" + languageCode + "'";
+ return req.response().result(message).code(500);
+ }
+
+ if (!translations.get(languageCode).containsKey(term)) {
+ String message = "Error: Invalid translation term '" + term + "'";
+ return req.response().result(message).code(500);
+ }
+
+ String message = translations.get(languageCode).get(term);
+ String capMessage = message.substring(0, 1).toUpperCase() + message.substring(1);
+ String response = String.format("%s", capMessage);
+ System.out.println(response);
+
+ return U.str(response);
+ }
+ }
+
+ private static Map> loadTranslations() {
+ Map> translations = new HashMap<>();
+
+ // German translations
+ Map germanTranslations = new HashMap<>();
+ germanTranslations.put("hello", "hallo");
+ germanTranslations.put("goodbye", "auf wiedersehen");
+ germanTranslations.put("thanks", "danke schön");
+ translations.put("de", germanTranslations);
+
+ // Spanish translations
+ Map spanishTranslations = new HashMap<>();
+ spanishTranslations.put("hello", "hola");
+ spanishTranslations.put("goodbye", "adiós");
+ spanishTranslations.put("thanks", "gracias");
+ translations.put("es", spanishTranslations);
+
+ // French translations
+ Map frenchTranslations = new HashMap<>();
+ frenchTranslations.put("hello", "bonjour");
+ frenchTranslations.put("goodbye", "au revoir");
+ frenchTranslations.put("thanks", "merci");
+ translations.put("fr", frenchTranslations);
+
+ // Latvian translations
+ Map latvianTranslations = new HashMap<>();
+ latvianTranslations.put("hello", "sveiks");
+ latvianTranslations.put("goodbye", "ardievu");
+ latvianTranslations.put("thanks", "paldies");
+ translations.put("lv", latvianTranslations);
+
+ // Maori translations
+ Map maoriTranslations = new HashMap<>();
+ maoriTranslations.put("hello", "kia ora");
+ maoriTranslations.put("goodbye", "poroporoaki");
+ maoriTranslations.put("thanks", "whakawhetai koe");
+ translations.put("mi", maoriTranslations);
+
+ // Slovak translations
+ Map slovakTranslations = new HashMap<>();
+ slovakTranslations.put("hello", "ahoj");
+ slovakTranslations.put("goodbye", "zbohom");
+ slovakTranslations.put("thanks", "ďakujem koe");
+ translations.put("sk", slovakTranslations);
+
+ // Turkish translations
+ Map turkishTranslations = new HashMap<>();
+ turkishTranslations.put("hello", "merhaba");
+ turkishTranslations.put("goodbye", "güle güle");
+ turkishTranslations.put("thanks", "teşekkür ederim");
+ translations.put("tr", turkishTranslations);
+
+ // Zulu translations
+ Map zuluTranslations = new HashMap<>();
+ zuluTranslations.put("hello", "hamba kahle");
+ zuluTranslations.put("goodbye", "sawubona");
+ zuluTranslations.put("thanks", "ngiyabonga");
+ translations.put("zu", zuluTranslations);
+
+ return translations;
+ }
+}