|
| 1 | +/* |
| 2 | + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 3 | + * |
| 4 | + * Modifications copyright (C) 2017 Uber Technologies, Inc. |
| 5 | + * |
| 6 | + * Licensed under the Apache License, Version 2.0 (the "License"). You may not |
| 7 | + * use this file except in compliance with the License. A copy of the License is |
| 8 | + * located at |
| 9 | + * |
| 10 | + * http://aws.amazon.com/apache2.0 |
| 11 | + * |
| 12 | + * or in the "license" file accompanying this file. This file is distributed on |
| 13 | + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either |
| 14 | + * express or implied. See the License for the specific language governing |
| 15 | + * permissions and limitations under the License. |
| 16 | + */ |
| 17 | + |
| 18 | +package com.uber.cadence.samples.query; |
| 19 | + |
| 20 | +import static com.uber.cadence.samples.query.QueryConstants.CLUSTER; |
| 21 | +import static com.uber.cadence.samples.query.QueryConstants.DOMAIN; |
| 22 | +import static com.uber.cadence.samples.query.QueryConstants.TASK_LIST; |
| 23 | + |
| 24 | +import com.uber.cadence.workflow.QueryMethod; |
| 25 | +import com.uber.cadence.workflow.SignalMethod; |
| 26 | +import com.uber.cadence.workflow.Workflow; |
| 27 | +import com.uber.cadence.workflow.WorkflowMethod; |
| 28 | +import java.time.Duration; |
| 29 | +import java.util.ArrayList; |
| 30 | +import java.util.List; |
| 31 | + |
| 32 | +/** |
| 33 | + * Demonstrates interactive voting via Cadence Web (v4.0.14+). Signals accumulate votes and the |
| 34 | + * query named {@code options} renders current results and vote buttons as markdown. |
| 35 | + */ |
| 36 | +public final class LunchVoteWorkflow { |
| 37 | + |
| 38 | + private LunchVoteWorkflow() {} |
| 39 | + |
| 40 | + /** |
| 41 | + * Signal payload for a lunch vote. Public fields are required so Cadence's JSON data converter |
| 42 | + * can deserialize the signal input. Field names must match the JSON keys in the Markdoc |
| 43 | + * {@code input=} attribute (e.g. {@code input={"location":"Farmhouse","meal":"Red Thai Curry"}}). |
| 44 | + */ |
| 45 | + public static class LunchOrder { |
| 46 | + public String location; |
| 47 | + public String meal; |
| 48 | + public String requests; |
| 49 | + |
| 50 | + /** No-arg constructor required for JSON deserialization. */ |
| 51 | + public LunchOrder() {} |
| 52 | + |
| 53 | + public LunchOrder(String location, String meal, String requests) { |
| 54 | + this.location = location; |
| 55 | + this.meal = meal; |
| 56 | + this.requests = requests; |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * The voting pattern: the workflow method keeps the execution open for a voting window, signals |
| 62 | + * mutate state (accumulate votes), and the query reads that state to render the current results. |
| 63 | + */ |
| 64 | + public interface WorkflowIface { |
| 65 | + |
| 66 | + @WorkflowMethod( |
| 67 | + name = QueryConstants.LUNCH_VOTE_WORKFLOW_TYPE, |
| 68 | + executionStartToCloseTimeoutSeconds = 700, |
| 69 | + taskList = TASK_LIST) |
| 70 | + void run(); |
| 71 | + |
| 72 | + /** Visible as "options" in the Cadence Web Query dropdown. */ |
| 73 | + @QueryMethod(name = "options") |
| 74 | + MarkdownFormattedResponse optionsQuery(); |
| 75 | + |
| 76 | + /** |
| 77 | + * {@code name} sets the signal type string the worker listens for. It must match the |
| 78 | + * {@code signalName} attribute in the Markdoc template so Cadence Web sends the right signal. |
| 79 | + */ |
| 80 | + @SignalMethod(name = "lunch_order") |
| 81 | + void lunchOrder(LunchOrder vote); |
| 82 | + } |
| 83 | + |
| 84 | + public static final class WorkflowImpl implements WorkflowIface { |
| 85 | + |
| 86 | + private final List<LunchOrder> votes = new ArrayList<>(); |
| 87 | + |
| 88 | + /** Cached on the workflow thread; queries must not call {@link Workflow#getWorkflowInfo()}. */ |
| 89 | + private String cachedWorkflowId = ""; |
| 90 | + |
| 91 | + private String cachedRunId = ""; |
| 92 | + |
| 93 | + @Override |
| 94 | + public void run() { |
| 95 | + cachedWorkflowId = Workflow.getWorkflowInfo().getWorkflowId(); |
| 96 | + cachedRunId = Workflow.getWorkflowInfo().getRunId(); |
| 97 | + // Keep the workflow open for the voting period. Votes arrive via the lunchOrder signal |
| 98 | + // while the workflow sleeps; Workflow.sleep is interruptible by signals. |
| 99 | + Workflow.sleep(Duration.ofMinutes(10)); |
| 100 | + } |
| 101 | + |
| 102 | + @Override |
| 103 | + public MarkdownFormattedResponse optionsQuery() { |
| 104 | + String workflowId = cachedWorkflowId; |
| 105 | + String runId = cachedRunId; |
| 106 | + String voteTable = makeLunchVoteTable(votes); |
| 107 | + String menuTable = makeLunchMenu(); |
| 108 | + |
| 109 | + String data = |
| 110 | + "\n## Lunch Options\n\n" |
| 111 | + + "We're voting on where to order lunch today. Select the option you want to vote for.\n\n" |
| 112 | + + "---\n\n" |
| 113 | + + "### Current Votes\n\n" |
| 114 | + + voteTable |
| 115 | + + "\n" |
| 116 | + + "### Menu Options\n\n" |
| 117 | + + menuTable |
| 118 | + + "\n\n---\n\n" |
| 119 | + + "### Cast Your Vote\n\n" |
| 120 | + + signalBlock( |
| 121 | + workflowId, |
| 122 | + runId, |
| 123 | + "Farmhouse - Red Thai Curry", |
| 124 | + "{\"location\":\"Farmhouse\",\"meal\":\"Red Thai Curry\",\"requests\":\"spicy\"}") |
| 125 | + + signalBlock( |
| 126 | + workflowId, |
| 127 | + runId, |
| 128 | + "Ethiopian Wat", |
| 129 | + "{\"location\":\"Ethiopian\",\"meal\":\"Wat with Injera\",\"requests\":\"\"}") |
| 130 | + + signalBlock( |
| 131 | + workflowId, |
| 132 | + runId, |
| 133 | + "Ler Ros - Tofu Bahn Mi", |
| 134 | + "{\"location\":\"Ler Ros\",\"meal\":\"Tofu Bahn Mi\",\"requests\":\"\"}") |
| 135 | + + "\n{% br /%}\n\n" |
| 136 | + + "*Vote closes when workflow times out (10 minutes)*\n\t"; |
| 137 | + |
| 138 | + return new MarkdownFormattedResponse(data); |
| 139 | + } |
| 140 | + |
| 141 | + /** Builds a Markdoc {@code {%- signal -%}} tag. Every attribute is required for Cadence Web |
| 142 | + * to route the signal to the correct workflow execution. */ |
| 143 | + private static String signalBlock( |
| 144 | + String workflowId, String runId, String label, String jsonInput) { |
| 145 | + return "{% signal \n" |
| 146 | + + "\tsignalName=\"lunch_order\" \n" |
| 147 | + + "\tlabel=\"" |
| 148 | + + label |
| 149 | + + "\"\n" |
| 150 | + + "\tdomain=\"" |
| 151 | + + DOMAIN |
| 152 | + + "\"\n" |
| 153 | + + "\tcluster=\"" |
| 154 | + + CLUSTER |
| 155 | + + "\"\n" |
| 156 | + + "\tworkflowId=\"" |
| 157 | + + workflowId |
| 158 | + + "\"\n" |
| 159 | + + "\trunId=\"" |
| 160 | + + runId |
| 161 | + + "\"\n" |
| 162 | + + "\tinput=" |
| 163 | + + jsonInput |
| 164 | + + "\n/%}\n"; |
| 165 | + } |
| 166 | + |
| 167 | + @Override |
| 168 | + public void lunchOrder(LunchOrder vote) { |
| 169 | + votes.add(vote); |
| 170 | + } |
| 171 | + |
| 172 | + private static String makeLunchVoteTable(List<LunchOrder> votes) { |
| 173 | + if (votes.isEmpty()) { |
| 174 | + return "| Location | Meal | Requests |\n|----------|------|----------|\n| *No votes yet* | | |\n"; |
| 175 | + } |
| 176 | + StringBuilder table = new StringBuilder(); |
| 177 | + table.append("| Location | Meal | Requests |\n|----------|------|----------|\n"); |
| 178 | + for (LunchOrder vote : votes) { |
| 179 | + table |
| 180 | + .append("| ") |
| 181 | + .append(vote.location != null ? vote.location : "") |
| 182 | + .append(" | ") |
| 183 | + .append(vote.meal != null ? vote.meal : "") |
| 184 | + .append(" | ") |
| 185 | + .append(vote.requests != null ? vote.requests : "") |
| 186 | + .append(" |\n"); |
| 187 | + } |
| 188 | + return table.toString(); |
| 189 | + } |
| 190 | + |
| 191 | + private static String makeLunchMenu() { |
| 192 | + return "| Picture | Description |\n" |
| 193 | + + "|---------|-------------|\n" |
| 194 | + + "| {% image src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Red_roast_duck_curry.jpg/200px-Red_roast_duck_curry.jpg\" alt=\"Red Thai Curry\" width=\"200\" /%} | **Farmhouse - Red Thai Curry**: A dish in Thai cuisine made from curry paste, coconut milk, meat, seafood, vegetables, and herbs. |\n" |
| 195 | + + "| {% image src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png/200px-B%C3%A1nh_m%C3%AC_th%E1%BB%8Bt_n%C6%B0%E1%BB%9Bng.png\" alt=\"Tofu Bahn Mi\" width=\"200\" /%} | **Ler Ros - Tofu Bahn Mi**: A Vietnamese sandwich with a baguette filled with lemongrass tofu, vegetables, and fresh herbs. |\n" |
| 196 | + + "| {% image src=\"https://upload.wikimedia.org/wikipedia/commons/thumb/5/54/Ethiopian_wat.jpg/960px-Ethiopian_wat.jpg\" alt=\"Ethiopian Wat\" width=\"200\" /%} | **Ethiopian Wat**: A traditional Ethiopian stew made from spices, vegetables, and legumes, served with injera flatbread. |\n" |
| 197 | + + "\n*(source: wikipedia)*"; |
| 198 | + } |
| 199 | + } |
| 200 | +} |
0 commit comments