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; + } +}