Skip to contentSkip to navigationSkip to topbar
On this page

Dynamic Call Center with Java and Servlets


In this tutorial we will show how to automate the routing of calls from customers to your support agents. Customers will be able to select a product and wait while TaskRouter tries to contact a product specialist for the best support experience. If no one is available, our application will save the customer's number and selected product so an agent can call them back later on.


This is what the application does at a high level

this-is-what-the-application-does-at-a-high-level page anchor
  1. Configure a workspace using the Twilio TaskRouter REST API .
  2. Listen for incoming calls and let the user select a product with the dial pad.
  3. Create a Task with the selected product and let TaskRouter handle it.
  4. Store missed calls so agents can return the call to customers.

In order to instruct TaskRouter to handle the Tasks, we need to configure a Workspace. We can do this in the TaskRouter Console(link takes you to an external page) or programmatically using the TaskRouter REST API.

A Workspace is the container element for any TaskRouter application. The elements are:

  • Tasks - Represents a customer trying to contact an agent.
  • Workers - The agents responsible for handling Tasks.
  • Task Queues - Holds Tasks to be consumed by a set of Workers.
  • Workflows - Responsible for placing Tasks into Task Queues.
  • Activities - Possible states of a Worker, e.g. Idle, Offline, Busy.

Configuring the Workspace

configuring-the-workspace page anchor

/src/main/resources/workspace.json

1
{
2
"name": "Twilio Workspace",
3
"event_callback": "%(host)s/events",
4
"workers": [
5
{
6
"name": "Bob",
7
"attributes": {
8
"products": [
9
"ProgrammableSMS"
10
],
11
"contact_uri": "%(bob_number)s"
12
}
13
},
14
{
15
"name": "Alice",
16
"attributes": {
17
"products": [
18
"ProgrammableVoice"
19
],
20
"contact_uri": "%(alice_number)s"
21
}
22
}
23
],
24
"activities": [
25
{
26
"name": "Offline",
27
"availability": "false"
28
},
29
{
30
"name": "Idle",
31
"availability": "true"
32
},
33
{
34
"name": "Busy",
35
"availability": "false"
36
},
37
{
38
"name": "Reserved",
39
"availability": "false"
40
}
41
],
42
"task_queues": [
43
{
44
"name": "Default",
45
"targetWorkers": "1==1"
46
},
47
{
48
"name": "SMS",
49
"targetWorkers": "products HAS \"ProgrammableSMS\""
50
},
51
{
52
"name": "Voice",
53
"targetWorkers": "products HAS \"ProgrammableVoice\""
54
}
55
],
56
"workflow": {
57
"name": "Sales",
58
"callback": "%(host)s/assignment",
59
"timeout": "15",
60
"routingConfiguration": [
61
{
62
"expression": "selected_product==\"ProgrammableSMS\"",
63
"targetTaskQueue": "SMS"
64
},
65
{
66
"expression": "selected_product==\"ProgrammableVoice\"",
67
"targetTaskQueue": "Voice"
68
}
69
]
70
}
71
}

In order to build a client for this API, we need as system variables a TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN which you can find on Twilio Console. The class TwilioAppSettings creates a TwilioTaskRouterClient, which is provided by the Twilio Java library(link takes you to an external page). This client is used by WorkspaceFacade which encapsulates all logic related to the Workspace class.

Let's take a look at a Gradle task that will handle the Workspace setup for us.


The CreateWorkspace Gradle Task

the-createworkspace-gradle-task page anchor

In this application the Gradle task(link takes you to an external page) createWorkspace is used to orchestrate calls to our WorkspaceFacade class in order to handle a Workspace. CreateWorkspaceTask is the java main class behind the Gradle task. It uses data provided by workspace.json and expects 3 arguments in the following order:

  1. hostname - A public URL to which Twilio can send requests. This can be either a cloud service or ngrok(link takes you to an external page) , which can expose a local application to the internet.
  2. bobPhone - The telephone number of Bob, the Programmable SMS specialist.
  3. alicePhone - Same for Alice, the Programmable Voice specialist.

The function createWorkspaceConfig is used to load the configuration of the workspace from workspace.json.

CreateWorkspace Gradle Task

createworkspace-gradle-task page anchor

src/main/java/com/twilio/taskrouter/application/CreateWorkspaceTask.java

1
package com.twilio.taskrouter.application;
2
3
import com.google.inject.Guice;
4
import com.google.inject.Injector;
5
import com.twilio.rest.taskrouter.v1.workspace.Activity;
6
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
7
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
8
import com.twilio.taskrouter.WorkflowRule;
9
import com.twilio.taskrouter.WorkflowRuleTarget;
10
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
11
import com.twilio.taskrouter.domain.common.Utils;
12
import com.twilio.taskrouter.domain.error.TaskRouterException;
13
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
14
import org.apache.commons.lang3.StringUtils;
15
import org.apache.commons.lang3.text.StrSubstitutor;
16
17
import javax.json.Json;
18
import javax.json.JsonArray;
19
import javax.json.JsonObject;
20
import javax.json.JsonReader;
21
import java.io.File;
22
import java.io.IOException;
23
import java.io.StringReader;
24
import java.net.URISyntaxException;
25
import java.net.URL;
26
import java.util.Arrays;
27
import java.util.HashMap;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Optional;
31
import java.util.Properties;
32
import java.util.logging.Logger;
33
import java.util.stream.Collectors;
34
35
import static java.lang.System.exit;
36
37
//import org.apache.commons.lang3.StringUtils;
38
//import org.apache.commons.lang3.text.StrSubstitutor;
39
40
/**
41
* Creates a workspace
42
*/
43
class CreateWorkspaceTask {
44
45
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
46
47
public static void main(String[] args) {
48
49
System.out.println("Creating workspace...");
50
if (args.length < 3) {
51
System.out.println("You must specify 3 parameters:");
52
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
53
System.out.println("- Phone of the first agent (Bob)");
54
System.out.println("- Phone of the secondary agent (Alice)");
55
exit(1);
56
}
57
58
String hostname = args[0];
59
String bobPhone = args[1];
60
String alicePhone = args[2];
61
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
62
hostname, bobPhone, alicePhone));
63
64
//Get the configuration
65
JsonObject workspaceConfig = createWorkspaceConfig(args);
66
67
//Get or Create the Workspace
68
Injector injector = Guice.createInjector();
69
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
70
71
String workspaceName = workspaceConfig.getString("name");
72
Map<String, String> workspaceParams = new HashMap<>();
73
workspaceParams.put("FriendlyName", workspaceName);
74
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
75
76
try {
77
WorkspaceFacade workspaceFacade = WorkspaceFacade
78
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
79
80
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
81
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
82
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
83
84
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
85
} catch (TaskRouterException e) {
86
LOG.severe(e.getMessage());
87
exit(1);
88
}
89
}
90
91
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
92
JsonObject workspaceJsonConfig) {
93
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
94
Activity idleActivity = workspaceFacade.getIdleActivity();
95
96
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
97
Map<String, String> workerParams = new HashMap<>();
98
workerParams.put("FriendlyName", workerJson.getString("name"));
99
workerParams.put("ActivitySid", idleActivity.getSid());
100
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
101
102
try {
103
workspaceFacade.addWorker(workerParams);
104
} catch (TaskRouterException e) {
105
LOG.warning(e.getMessage());
106
}
107
});
108
}
109
110
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
111
JsonObject workspaceJsonConfig) {
112
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
113
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
114
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
115
+ "TaskQueues cannot be added."));
116
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
117
new TaskRouterException("The activity for assignments 'Busy' was not found. "
118
+ "TaskQueues cannot be added."));
119
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
120
Map<String, String> taskQueueParams = new HashMap<>();
121
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
122
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
123
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
124
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
125
126
try {
127
workspaceFacade.addTaskQueue(taskQueueParams);
128
} catch (TaskRouterException e) {
129
LOG.warning(e.getMessage());
130
}
131
});
132
}
133
134
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
135
JsonObject workspaceConfig) {
136
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
137
String workflowName = workflowJson.getString("name");
138
return workspaceFacade.findWorkflowByName(workflowName)
139
.orElseGet(() -> {
140
Map<String, String> workflowParams = new HashMap<>();
141
workflowParams.put("FriendlyName", workflowName);
142
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
143
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
144
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
145
146
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
147
workflowParams.put("Configuration", workflowConfigJson);
148
149
return workspaceFacade.addWorkflow(workflowParams);
150
});
151
}
152
153
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
154
Workflow workflow,
155
TwilioAppSettings twilioSettings) {
156
Activity idleActivity = workspaceFacade.getIdleActivity();
157
158
Properties workspaceParams = new Properties();
159
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
160
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
161
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
162
workspaceParams.put("workflow.sid", workflow.getSid());
163
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
164
workspaceParams.put("email", twilioSettings.getEmail());
165
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
166
167
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
168
169
try {
170
Utils.saveProperties(workspaceParams,
171
workspacePropertiesFile,
172
"Properties for last created Twilio TaskRouter workspace");
173
} catch (IOException e) {
174
LOG.severe("Could not save workspace.properties with current configuration");
175
exit(1);
176
}
177
178
String successMsg = String.format("Workspace '%s' was created successfully.",
179
workspaceFacade.getFriendlyName());
180
final int lineLength = successMsg.length() + 2;
181
182
System.out.println(StringUtils.repeat("#", lineLength));
183
System.out.println(String.format(" %s ", successMsg));
184
System.out.println(StringUtils.repeat("#", lineLength));
185
System.out.println("The following variables were registered:");
186
System.out.println("\n");
187
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
188
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
189
});
190
System.out.println("\n");
191
System.out.println(StringUtils.repeat("#", lineLength));
192
}
193
194
public static JsonObject createWorkspaceConfig(String[] args) {
195
final String configFileName = "workspace.json";
196
197
Optional<URL> url =
198
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
199
return url.map(u -> {
200
try {
201
File workspaceConfigJsonFile = new File(u.toURI());
202
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
203
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
204
205
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
206
return jsonReader.readObject();
207
}
208
} catch (URISyntaxException e) {
209
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
210
configFileName, e.getMessage()));
211
} catch (IOException e) {
212
throw new TaskRouterException(String.format("Error while reading %s: %s",
213
configFileName, e.getMessage()));
214
}
215
}).orElseThrow(
216
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
217
}
218
219
private static String parseWorkspaceJsonContent(final String unparsedContent,
220
final String... args) {
221
Map<String, String> values = new HashMap<>();
222
values.put("host", args[0]);
223
values.put("bob_number", args[1]);
224
values.put("alice_number", args[2]);
225
226
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
227
return strSubstitutor.replace(unparsedContent);
228
}
229
230
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
231
JsonObject workflowJson) {
232
try {
233
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
234
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
235
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
236
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
237
.expression("1=1")
238
.priority(1)
239
.timeout(30)
240
.build();
241
242
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
243
.map(ruleJson -> {
244
String ruleQueueName = ruleJson.getString("targetTaskQueue");
245
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
246
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
247
248
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
249
.priority(5)
250
.timeout(30)
251
.build();
252
253
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
254
255
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
256
}).collect(Collectors.toList());
257
258
com.twilio.taskrouter.Workflow config;
259
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
260
return config.toJson();
261
} catch (Exception ex) {
262
throw new TaskRouterException("Error while creating workflow json configuration", ex);
263
}
264
}
265
}

Now let's look in more detail at all the steps, starting with the creation of the workspace itself.


Before creating a workspace, we need to delete any others with the same FriendlyName as identifier. In order to create a workspace we need to provide a FriendlyName, and a EventCallbackUrl which contains an URL to be called every time an event is triggered in the workspace.

src/main/java/com/twilio/taskrouter/domain/model/WorkspaceFacade.java

1
package com.twilio.taskrouter.domain.model;
2
3
import com.fasterxml.jackson.databind.ObjectMapper;
4
import com.twilio.base.ResourceSet;
5
import com.twilio.http.TwilioRestClient;
6
import com.twilio.rest.taskrouter.v1.Workspace;
7
import com.twilio.rest.taskrouter.v1.WorkspaceCreator;
8
import com.twilio.rest.taskrouter.v1.WorkspaceDeleter;
9
import com.twilio.rest.taskrouter.v1.WorkspaceFetcher;
10
import com.twilio.rest.taskrouter.v1.WorkspaceReader;
11
import com.twilio.rest.taskrouter.v1.workspace.Activity;
12
import com.twilio.rest.taskrouter.v1.workspace.ActivityReader;
13
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
14
import com.twilio.rest.taskrouter.v1.workspace.TaskQueueCreator;
15
import com.twilio.rest.taskrouter.v1.workspace.TaskQueueReader;
16
import com.twilio.rest.taskrouter.v1.workspace.Worker;
17
import com.twilio.rest.taskrouter.v1.workspace.WorkerCreator;
18
import com.twilio.rest.taskrouter.v1.workspace.WorkerReader;
19
import com.twilio.rest.taskrouter.v1.workspace.WorkerUpdater;
20
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
21
import com.twilio.rest.taskrouter.v1.workspace.WorkflowCreator;
22
import com.twilio.rest.taskrouter.v1.workspace.WorkflowReader;
23
import com.twilio.taskrouter.domain.error.TaskRouterException;
24
25
import java.io.IOException;
26
import java.util.HashMap;
27
import java.util.Map;
28
import java.util.Optional;
29
import java.util.stream.StreamSupport;
30
31
public class WorkspaceFacade {
32
33
private final TwilioRestClient client;
34
35
private final Workspace workspace;
36
37
private Activity idleActivity;
38
39
private Map<String, Worker> phoneToWorker;
40
41
public WorkspaceFacade(TwilioRestClient client, Workspace workspace) {
42
this.client = client;
43
this.workspace = workspace;
44
}
45
46
public static WorkspaceFacade create(TwilioRestClient client,
47
Map<String, String> params) {
48
String workspaceName = params.get("FriendlyName");
49
String eventCallbackUrl = params.get("EventCallbackUrl");
50
51
ResourceSet<Workspace> execute = new WorkspaceReader()
52
.setFriendlyName(workspaceName)
53
.read(client);
54
StreamSupport.stream(execute.spliterator(), false)
55
.findFirst()
56
.ifPresent(workspace -> new WorkspaceDeleter(workspace.getSid()).delete(client));
57
58
Workspace workspace = new WorkspaceCreator(workspaceName)
59
.setEventCallbackUrl(eventCallbackUrl)
60
.create(client);
61
62
return new WorkspaceFacade(client, workspace);
63
}
64
65
public static Optional<WorkspaceFacade> findBySid(String workspaceSid,
66
TwilioRestClient client) {
67
Workspace workspace = new WorkspaceFetcher(workspaceSid).fetch(client);
68
return Optional.of(new WorkspaceFacade(client, workspace));
69
}
70
71
public String getFriendlyName() {
72
return workspace.getFriendlyName();
73
}
74
75
public String getSid() {
76
return workspace.getSid();
77
}
78
79
public Worker addWorker(Map<String, String> workerParams) {
80
return new WorkerCreator(workspace.getSid(), workerParams.get("FriendlyName"))
81
.setActivitySid(workerParams.get("ActivitySid"))
82
.setAttributes(workerParams.get("Attributes"))
83
.create(client);
84
}
85
86
public void addTaskQueue(Map<String, String> taskQueueParams) {
87
new TaskQueueCreator(this.workspace.getSid(),
88
taskQueueParams.get("FriendlyName"),
89
taskQueueParams.get("ReservationActivitySid"),
90
taskQueueParams.get("AssignmentActivitySid"))
91
.create(client);
92
}
93
94
public Workflow addWorkflow(Map<String, String> workflowParams) {
95
return new WorkflowCreator(workspace.getSid(),
96
workflowParams.get("FriendlyName"),
97
workflowParams.get("Configuration"))
98
.setAssignmentCallbackUrl(workflowParams.get("AssignmentCallbackUrl"))
99
.setFallbackAssignmentCallbackUrl(workflowParams.get("FallbackAssignmentCallbackUrl"))
100
.setTaskReservationTimeout(Integer.valueOf(workflowParams.get("TaskReservationTimeout")))
101
.create(client);
102
}
103
104
public Optional<Activity> findActivityByName(String activityName) {
105
return StreamSupport.stream(new ActivityReader(this.workspace.getSid())
106
.setFriendlyName(activityName)
107
.read(client).spliterator(), false
108
).findFirst();
109
}
110
111
public Optional<TaskQueue> findTaskQueueByName(String queueName) {
112
return StreamSupport.stream(new TaskQueueReader(this.workspace.getSid())
113
.setFriendlyName(queueName)
114
.read(client).spliterator(), false
115
).findFirst();
116
}
117
118
public Optional<Workflow> findWorkflowByName(String workflowName) {
119
return StreamSupport.stream(new WorkflowReader(this.workspace.getSid())
120
.setFriendlyName(workflowName)
121
.read(client).spliterator(), false
122
).findFirst();
123
}
124
125
public Optional<Worker> findWorkerByPhone(String workerPhone) {
126
return Optional.ofNullable(getPhoneToWorker().get(workerPhone));
127
}
128
129
public Map<String, Worker> getPhoneToWorker() {
130
if (phoneToWorker == null) {
131
phoneToWorker = new HashMap<>();
132
StreamSupport.stream(
133
new WorkerReader(this.workspace.getSid()).read(client).spliterator(), false
134
).forEach(worker -> {
135
try {
136
HashMap<String, Object> attributes = new ObjectMapper()
137
.readValue(worker.getAttributes(), HashMap.class);
138
phoneToWorker.put(attributes.get("contact_uri").toString(), worker);
139
} catch (IOException e) {
140
throw new TaskRouterException(
141
String.format("'%s' has a malformed json attributes", worker.getFriendlyName()));
142
}
143
});
144
}
145
return phoneToWorker;
146
}
147
148
public Activity getIdleActivity() {
149
if (idleActivity == null) {
150
idleActivity = findActivityByName("Idle").get();
151
}
152
return idleActivity;
153
}
154
155
public void updateWorkerStatus(Worker worker, String activityFriendlyName) {
156
Activity activity = findActivityByName(activityFriendlyName).orElseThrow(() ->
157
new TaskRouterException(
158
String.format("The activity '%s' doesn't exist in the workspace", activityFriendlyName)
159
)
160
);
161
162
new WorkerUpdater(workspace.getSid(), worker.getSid())
163
.setActivitySid(activity.getSid())
164
.update(client);
165
}
166
}

We have a brand new workspace, now we need workers. Let's create them on the next step.


We'll create two workers: Bob and Alice. They each have two attributes: contact_uri a phone number and products, a list of products each worker is specialized in. We also need to specify an activity_sid and a name for each worker. The selected activity will define the status of the worker.

Creating the Workers

1
package com.twilio.taskrouter.application;
2
3
import com.google.inject.Guice;
4
import com.google.inject.Injector;
5
import com.twilio.rest.taskrouter.v1.workspace.Activity;
6
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
7
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
8
import com.twilio.taskrouter.WorkflowRule;
9
import com.twilio.taskrouter.WorkflowRuleTarget;
10
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
11
import com.twilio.taskrouter.domain.common.Utils;
12
import com.twilio.taskrouter.domain.error.TaskRouterException;
13
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
14
import org.apache.commons.lang3.StringUtils;
15
import org.apache.commons.lang3.text.StrSubstitutor;
16
17
import javax.json.Json;
18
import javax.json.JsonArray;
19
import javax.json.JsonObject;
20
import javax.json.JsonReader;
21
import java.io.File;
22
import java.io.IOException;
23
import java.io.StringReader;
24
import java.net.URISyntaxException;
25
import java.net.URL;
26
import java.util.Arrays;
27
import java.util.HashMap;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Optional;
31
import java.util.Properties;
32
import java.util.logging.Logger;
33
import java.util.stream.Collectors;
34
35
import static java.lang.System.exit;
36
37
//import org.apache.commons.lang3.StringUtils;
38
//import org.apache.commons.lang3.text.StrSubstitutor;
39
40
/**
41
* Creates a workspace
42
*/
43
class CreateWorkspaceTask {
44
45
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
46
47
public static void main(String[] args) {
48
49
System.out.println("Creating workspace...");
50
if (args.length < 3) {
51
System.out.println("You must specify 3 parameters:");
52
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
53
System.out.println("- Phone of the first agent (Bob)");
54
System.out.println("- Phone of the secondary agent (Alice)");
55
exit(1);
56
}
57
58
String hostname = args[0];
59
String bobPhone = args[1];
60
String alicePhone = args[2];
61
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
62
hostname, bobPhone, alicePhone));
63
64
//Get the configuration
65
JsonObject workspaceConfig = createWorkspaceConfig(args);
66
67
//Get or Create the Workspace
68
Injector injector = Guice.createInjector();
69
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
70
71
String workspaceName = workspaceConfig.getString("name");
72
Map<String, String> workspaceParams = new HashMap<>();
73
workspaceParams.put("FriendlyName", workspaceName);
74
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
75
76
try {
77
WorkspaceFacade workspaceFacade = WorkspaceFacade
78
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
79
80
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
81
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
82
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
83
84
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
85
} catch (TaskRouterException e) {
86
LOG.severe(e.getMessage());
87
exit(1);
88
}
89
}
90
91
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
92
JsonObject workspaceJsonConfig) {
93
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
94
Activity idleActivity = workspaceFacade.getIdleActivity();
95
96
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
97
Map<String, String> workerParams = new HashMap<>();
98
workerParams.put("FriendlyName", workerJson.getString("name"));
99
workerParams.put("ActivitySid", idleActivity.getSid());
100
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
101
102
try {
103
workspaceFacade.addWorker(workerParams);
104
} catch (TaskRouterException e) {
105
LOG.warning(e.getMessage());
106
}
107
});
108
}
109
110
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
111
JsonObject workspaceJsonConfig) {
112
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
113
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
114
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
115
+ "TaskQueues cannot be added."));
116
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
117
new TaskRouterException("The activity for assignments 'Busy' was not found. "
118
+ "TaskQueues cannot be added."));
119
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
120
Map<String, String> taskQueueParams = new HashMap<>();
121
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
122
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
123
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
124
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
125
126
try {
127
workspaceFacade.addTaskQueue(taskQueueParams);
128
} catch (TaskRouterException e) {
129
LOG.warning(e.getMessage());
130
}
131
});
132
}
133
134
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
135
JsonObject workspaceConfig) {
136
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
137
String workflowName = workflowJson.getString("name");
138
return workspaceFacade.findWorkflowByName(workflowName)
139
.orElseGet(() -> {
140
Map<String, String> workflowParams = new HashMap<>();
141
workflowParams.put("FriendlyName", workflowName);
142
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
143
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
144
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
145
146
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
147
workflowParams.put("Configuration", workflowConfigJson);
148
149
return workspaceFacade.addWorkflow(workflowParams);
150
});
151
}
152
153
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
154
Workflow workflow,
155
TwilioAppSettings twilioSettings) {
156
Activity idleActivity = workspaceFacade.getIdleActivity();
157
158
Properties workspaceParams = new Properties();
159
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
160
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
161
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
162
workspaceParams.put("workflow.sid", workflow.getSid());
163
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
164
workspaceParams.put("email", twilioSettings.getEmail());
165
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
166
167
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
168
169
try {
170
Utils.saveProperties(workspaceParams,
171
workspacePropertiesFile,
172
"Properties for last created Twilio TaskRouter workspace");
173
} catch (IOException e) {
174
LOG.severe("Could not save workspace.properties with current configuration");
175
exit(1);
176
}
177
178
String successMsg = String.format("Workspace '%s' was created successfully.",
179
workspaceFacade.getFriendlyName());
180
final int lineLength = successMsg.length() + 2;
181
182
System.out.println(StringUtils.repeat("#", lineLength));
183
System.out.println(String.format(" %s ", successMsg));
184
System.out.println(StringUtils.repeat("#", lineLength));
185
System.out.println("The following variables were registered:");
186
System.out.println("\n");
187
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
188
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
189
});
190
System.out.println("\n");
191
System.out.println(StringUtils.repeat("#", lineLength));
192
}
193
194
public static JsonObject createWorkspaceConfig(String[] args) {
195
final String configFileName = "workspace.json";
196
197
Optional<URL> url =
198
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
199
return url.map(u -> {
200
try {
201
File workspaceConfigJsonFile = new File(u.toURI());
202
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
203
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
204
205
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
206
return jsonReader.readObject();
207
}
208
} catch (URISyntaxException e) {
209
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
210
configFileName, e.getMessage()));
211
} catch (IOException e) {
212
throw new TaskRouterException(String.format("Error while reading %s: %s",
213
configFileName, e.getMessage()));
214
}
215
}).orElseThrow(
216
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
217
}
218
219
private static String parseWorkspaceJsonContent(final String unparsedContent,
220
final String... args) {
221
Map<String, String> values = new HashMap<>();
222
values.put("host", args[0]);
223
values.put("bob_number", args[1]);
224
values.put("alice_number", args[2]);
225
226
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
227
return strSubstitutor.replace(unparsedContent);
228
}
229
230
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
231
JsonObject workflowJson) {
232
try {
233
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
234
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
235
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
236
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
237
.expression("1=1")
238
.priority(1)
239
.timeout(30)
240
.build();
241
242
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
243
.map(ruleJson -> {
244
String ruleQueueName = ruleJson.getString("targetTaskQueue");
245
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
246
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
247
248
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
249
.priority(5)
250
.timeout(30)
251
.build();
252
253
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
254
255
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
256
}).collect(Collectors.toList());
257
258
com.twilio.taskrouter.Workflow config;
259
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
260
return config.toJson();
261
} catch (Exception ex) {
262
throw new TaskRouterException("Error while creating workflow json configuration", ex);
263
}
264
}
265
}

After creating our workers, let's set up the Task Queues.


Next, we set up the Task Queues. Each with a name and a TargetWorkers property, which is an expression to match Workers. Our Task Queues are:

  1. SMS - Will target Workers specialized in Programmable SMS, such as Bob, using the expression "products HAS \"ProgrammableSMS\"" .
  2. Voice - Will do the same for Programmable Voice Workers, such as Alice, using the expression "products HAS \"ProgrammableVoice\"" .
  3. All - This queue targets all users and can be used when there are no specialist around for the chosen product. We can use the "1==1" expression here.

Creating the Task Queues

1
package com.twilio.taskrouter.application;
2
3
import com.google.inject.Guice;
4
import com.google.inject.Injector;
5
import com.twilio.rest.taskrouter.v1.workspace.Activity;
6
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
7
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
8
import com.twilio.taskrouter.WorkflowRule;
9
import com.twilio.taskrouter.WorkflowRuleTarget;
10
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
11
import com.twilio.taskrouter.domain.common.Utils;
12
import com.twilio.taskrouter.domain.error.TaskRouterException;
13
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
14
import org.apache.commons.lang3.StringUtils;
15
import org.apache.commons.lang3.text.StrSubstitutor;
16
17
import javax.json.Json;
18
import javax.json.JsonArray;
19
import javax.json.JsonObject;
20
import javax.json.JsonReader;
21
import java.io.File;
22
import java.io.IOException;
23
import java.io.StringReader;
24
import java.net.URISyntaxException;
25
import java.net.URL;
26
import java.util.Arrays;
27
import java.util.HashMap;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Optional;
31
import java.util.Properties;
32
import java.util.logging.Logger;
33
import java.util.stream.Collectors;
34
35
import static java.lang.System.exit;
36
37
//import org.apache.commons.lang3.StringUtils;
38
//import org.apache.commons.lang3.text.StrSubstitutor;
39
40
/**
41
* Creates a workspace
42
*/
43
class CreateWorkspaceTask {
44
45
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
46
47
public static void main(String[] args) {
48
49
System.out.println("Creating workspace...");
50
if (args.length < 3) {
51
System.out.println("You must specify 3 parameters:");
52
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
53
System.out.println("- Phone of the first agent (Bob)");
54
System.out.println("- Phone of the secondary agent (Alice)");
55
exit(1);
56
}
57
58
String hostname = args[0];
59
String bobPhone = args[1];
60
String alicePhone = args[2];
61
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
62
hostname, bobPhone, alicePhone));
63
64
//Get the configuration
65
JsonObject workspaceConfig = createWorkspaceConfig(args);
66
67
//Get or Create the Workspace
68
Injector injector = Guice.createInjector();
69
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
70
71
String workspaceName = workspaceConfig.getString("name");
72
Map<String, String> workspaceParams = new HashMap<>();
73
workspaceParams.put("FriendlyName", workspaceName);
74
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
75
76
try {
77
WorkspaceFacade workspaceFacade = WorkspaceFacade
78
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
79
80
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
81
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
82
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
83
84
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
85
} catch (TaskRouterException e) {
86
LOG.severe(e.getMessage());
87
exit(1);
88
}
89
}
90
91
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
92
JsonObject workspaceJsonConfig) {
93
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
94
Activity idleActivity = workspaceFacade.getIdleActivity();
95
96
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
97
Map<String, String> workerParams = new HashMap<>();
98
workerParams.put("FriendlyName", workerJson.getString("name"));
99
workerParams.put("ActivitySid", idleActivity.getSid());
100
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
101
102
try {
103
workspaceFacade.addWorker(workerParams);
104
} catch (TaskRouterException e) {
105
LOG.warning(e.getMessage());
106
}
107
});
108
}
109
110
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
111
JsonObject workspaceJsonConfig) {
112
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
113
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
114
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
115
+ "TaskQueues cannot be added."));
116
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
117
new TaskRouterException("The activity for assignments 'Busy' was not found. "
118
+ "TaskQueues cannot be added."));
119
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
120
Map<String, String> taskQueueParams = new HashMap<>();
121
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
122
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
123
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
124
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
125
126
try {
127
workspaceFacade.addTaskQueue(taskQueueParams);
128
} catch (TaskRouterException e) {
129
LOG.warning(e.getMessage());
130
}
131
});
132
}
133
134
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
135
JsonObject workspaceConfig) {
136
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
137
String workflowName = workflowJson.getString("name");
138
return workspaceFacade.findWorkflowByName(workflowName)
139
.orElseGet(() -> {
140
Map<String, String> workflowParams = new HashMap<>();
141
workflowParams.put("FriendlyName", workflowName);
142
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
143
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
144
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
145
146
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
147
workflowParams.put("Configuration", workflowConfigJson);
148
149
return workspaceFacade.addWorkflow(workflowParams);
150
});
151
}
152
153
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
154
Workflow workflow,
155
TwilioAppSettings twilioSettings) {
156
Activity idleActivity = workspaceFacade.getIdleActivity();
157
158
Properties workspaceParams = new Properties();
159
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
160
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
161
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
162
workspaceParams.put("workflow.sid", workflow.getSid());
163
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
164
workspaceParams.put("email", twilioSettings.getEmail());
165
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
166
167
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
168
169
try {
170
Utils.saveProperties(workspaceParams,
171
workspacePropertiesFile,
172
"Properties for last created Twilio TaskRouter workspace");
173
} catch (IOException e) {
174
LOG.severe("Could not save workspace.properties with current configuration");
175
exit(1);
176
}
177
178
String successMsg = String.format("Workspace '%s' was created successfully.",
179
workspaceFacade.getFriendlyName());
180
final int lineLength = successMsg.length() + 2;
181
182
System.out.println(StringUtils.repeat("#", lineLength));
183
System.out.println(String.format(" %s ", successMsg));
184
System.out.println(StringUtils.repeat("#", lineLength));
185
System.out.println("The following variables were registered:");
186
System.out.println("\n");
187
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
188
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
189
});
190
System.out.println("\n");
191
System.out.println(StringUtils.repeat("#", lineLength));
192
}
193
194
public static JsonObject createWorkspaceConfig(String[] args) {
195
final String configFileName = "workspace.json";
196
197
Optional<URL> url =
198
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
199
return url.map(u -> {
200
try {
201
File workspaceConfigJsonFile = new File(u.toURI());
202
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
203
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
204
205
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
206
return jsonReader.readObject();
207
}
208
} catch (URISyntaxException e) {
209
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
210
configFileName, e.getMessage()));
211
} catch (IOException e) {
212
throw new TaskRouterException(String.format("Error while reading %s: %s",
213
configFileName, e.getMessage()));
214
}
215
}).orElseThrow(
216
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
217
}
218
219
private static String parseWorkspaceJsonContent(final String unparsedContent,
220
final String... args) {
221
Map<String, String> values = new HashMap<>();
222
values.put("host", args[0]);
223
values.put("bob_number", args[1]);
224
values.put("alice_number", args[2]);
225
226
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
227
return strSubstitutor.replace(unparsedContent);
228
}
229
230
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
231
JsonObject workflowJson) {
232
try {
233
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
234
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
235
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
236
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
237
.expression("1=1")
238
.priority(1)
239
.timeout(30)
240
.build();
241
242
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
243
.map(ruleJson -> {
244
String ruleQueueName = ruleJson.getString("targetTaskQueue");
245
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
246
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
247
248
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
249
.priority(5)
250
.timeout(30)
251
.build();
252
253
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
254
255
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
256
}).collect(Collectors.toList());
257
258
com.twilio.taskrouter.Workflow config;
259
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
260
return config.toJson();
261
} catch (Exception ex) {
262
throw new TaskRouterException("Error while creating workflow json configuration", ex);
263
}
264
}
265
}

We have a Workspace, Workers and Task Queues... what's left? A Workflow. Let's see how to create one next!


Finally, we set up the Workflow using the following parameters:

  1. FriendlyName as the name of a Workflow.
  2. AssignmentCallbackUrl as the public URL where a request will be made when this Workflow assigns a Task to a Worker. We will learn how to implement it on the next steps.
  3. TaskReservationTimeout as the maximum time we want to wait until a Worker handles a Task.
  4. configuration which is a set of rules for placing Task into Task Queues. The routing configuration will take a Task's attribute and match this with Task Queues. This application's Workflow rules are defined as:

    • selected_product=="ProgrammableSMS" expression for SMS Task Queue. This expression will match any Task with ProgrammableSMS as the selected_product attribute.
    • selected_product=="ProgrammableVoice expression for Voice Task Queue.

Creating a Workflow

1
package com.twilio.taskrouter.application;
2
3
import com.google.inject.Guice;
4
import com.google.inject.Injector;
5
import com.twilio.rest.taskrouter.v1.workspace.Activity;
6
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
7
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
8
import com.twilio.taskrouter.WorkflowRule;
9
import com.twilio.taskrouter.WorkflowRuleTarget;
10
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
11
import com.twilio.taskrouter.domain.common.Utils;
12
import com.twilio.taskrouter.domain.error.TaskRouterException;
13
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
14
import org.apache.commons.lang3.StringUtils;
15
import org.apache.commons.lang3.text.StrSubstitutor;
16
17
import javax.json.Json;
18
import javax.json.JsonArray;
19
import javax.json.JsonObject;
20
import javax.json.JsonReader;
21
import java.io.File;
22
import java.io.IOException;
23
import java.io.StringReader;
24
import java.net.URISyntaxException;
25
import java.net.URL;
26
import java.util.Arrays;
27
import java.util.HashMap;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Optional;
31
import java.util.Properties;
32
import java.util.logging.Logger;
33
import java.util.stream.Collectors;
34
35
import static java.lang.System.exit;
36
37
//import org.apache.commons.lang3.StringUtils;
38
//import org.apache.commons.lang3.text.StrSubstitutor;
39
40
/**
41
* Creates a workspace
42
*/
43
class CreateWorkspaceTask {
44
45
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
46
47
public static void main(String[] args) {
48
49
System.out.println("Creating workspace...");
50
if (args.length < 3) {
51
System.out.println("You must specify 3 parameters:");
52
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
53
System.out.println("- Phone of the first agent (Bob)");
54
System.out.println("- Phone of the secondary agent (Alice)");
55
exit(1);
56
}
57
58
String hostname = args[0];
59
String bobPhone = args[1];
60
String alicePhone = args[2];
61
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
62
hostname, bobPhone, alicePhone));
63
64
//Get the configuration
65
JsonObject workspaceConfig = createWorkspaceConfig(args);
66
67
//Get or Create the Workspace
68
Injector injector = Guice.createInjector();
69
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
70
71
String workspaceName = workspaceConfig.getString("name");
72
Map<String, String> workspaceParams = new HashMap<>();
73
workspaceParams.put("FriendlyName", workspaceName);
74
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
75
76
try {
77
WorkspaceFacade workspaceFacade = WorkspaceFacade
78
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
79
80
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
81
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
82
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
83
84
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
85
} catch (TaskRouterException e) {
86
LOG.severe(e.getMessage());
87
exit(1);
88
}
89
}
90
91
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
92
JsonObject workspaceJsonConfig) {
93
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
94
Activity idleActivity = workspaceFacade.getIdleActivity();
95
96
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
97
Map<String, String> workerParams = new HashMap<>();
98
workerParams.put("FriendlyName", workerJson.getString("name"));
99
workerParams.put("ActivitySid", idleActivity.getSid());
100
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
101
102
try {
103
workspaceFacade.addWorker(workerParams);
104
} catch (TaskRouterException e) {
105
LOG.warning(e.getMessage());
106
}
107
});
108
}
109
110
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
111
JsonObject workspaceJsonConfig) {
112
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
113
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
114
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
115
+ "TaskQueues cannot be added."));
116
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
117
new TaskRouterException("The activity for assignments 'Busy' was not found. "
118
+ "TaskQueues cannot be added."));
119
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
120
Map<String, String> taskQueueParams = new HashMap<>();
121
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
122
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
123
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
124
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
125
126
try {
127
workspaceFacade.addTaskQueue(taskQueueParams);
128
} catch (TaskRouterException e) {
129
LOG.warning(e.getMessage());
130
}
131
});
132
}
133
134
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
135
JsonObject workspaceConfig) {
136
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
137
String workflowName = workflowJson.getString("name");
138
return workspaceFacade.findWorkflowByName(workflowName)
139
.orElseGet(() -> {
140
Map<String, String> workflowParams = new HashMap<>();
141
workflowParams.put("FriendlyName", workflowName);
142
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
143
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
144
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
145
146
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
147
workflowParams.put("Configuration", workflowConfigJson);
148
149
return workspaceFacade.addWorkflow(workflowParams);
150
});
151
}
152
153
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
154
Workflow workflow,
155
TwilioAppSettings twilioSettings) {
156
Activity idleActivity = workspaceFacade.getIdleActivity();
157
158
Properties workspaceParams = new Properties();
159
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
160
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
161
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
162
workspaceParams.put("workflow.sid", workflow.getSid());
163
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
164
workspaceParams.put("email", twilioSettings.getEmail());
165
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
166
167
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
168
169
try {
170
Utils.saveProperties(workspaceParams,
171
workspacePropertiesFile,
172
"Properties for last created Twilio TaskRouter workspace");
173
} catch (IOException e) {
174
LOG.severe("Could not save workspace.properties with current configuration");
175
exit(1);
176
}
177
178
String successMsg = String.format("Workspace '%s' was created successfully.",
179
workspaceFacade.getFriendlyName());
180
final int lineLength = successMsg.length() + 2;
181
182
System.out.println(StringUtils.repeat("#", lineLength));
183
System.out.println(String.format(" %s ", successMsg));
184
System.out.println(StringUtils.repeat("#", lineLength));
185
System.out.println("The following variables were registered:");
186
System.out.println("\n");
187
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
188
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
189
});
190
System.out.println("\n");
191
System.out.println(StringUtils.repeat("#", lineLength));
192
}
193
194
public static JsonObject createWorkspaceConfig(String[] args) {
195
final String configFileName = "workspace.json";
196
197
Optional<URL> url =
198
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
199
return url.map(u -> {
200
try {
201
File workspaceConfigJsonFile = new File(u.toURI());
202
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
203
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
204
205
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
206
return jsonReader.readObject();
207
}
208
} catch (URISyntaxException e) {
209
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
210
configFileName, e.getMessage()));
211
} catch (IOException e) {
212
throw new TaskRouterException(String.format("Error while reading %s: %s",
213
configFileName, e.getMessage()));
214
}
215
}).orElseThrow(
216
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
217
}
218
219
private static String parseWorkspaceJsonContent(final String unparsedContent,
220
final String... args) {
221
Map<String, String> values = new HashMap<>();
222
values.put("host", args[0]);
223
values.put("bob_number", args[1]);
224
values.put("alice_number", args[2]);
225
226
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
227
return strSubstitutor.replace(unparsedContent);
228
}
229
230
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
231
JsonObject workflowJson) {
232
try {
233
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
234
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
235
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
236
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
237
.expression("1=1")
238
.priority(1)
239
.timeout(30)
240
.build();
241
242
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
243
.map(ruleJson -> {
244
String ruleQueueName = ruleJson.getString("targetTaskQueue");
245
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
246
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
247
248
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
249
.priority(5)
250
.timeout(30)
251
.build();
252
253
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
254
255
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
256
}).collect(Collectors.toList());
257
258
com.twilio.taskrouter.Workflow config;
259
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
260
return config.toJson();
261
} catch (Exception ex) {
262
throw new TaskRouterException("Error while creating workflow json configuration", ex);
263
}
264
}
265
}

Our workspace is completely setup. Now it's time to see how we use it to route calls.


Handle Twilio's Request

handle-twilios-request page anchor

Right after receiving a call, Twilio will send a request to the URL specified on the number's configuration.

The endpoint will then process the request and generate a TwiML response. We'll use the Say verb to give the user product alternatives, and a key they can press in order to select one. The Gather verb allows us to capture the user's key press.

src/main/java/com/twilio/taskrouter/application/servlet/IncomingCallServlet.java

1
package com.twilio.taskrouter.application.servlet;
2
3
4
import com.twilio.twiml.Gather;
5
import com.twilio.twiml.Method;
6
import com.twilio.twiml.Say;
7
import com.twilio.twiml.TwiMLException;
8
import com.twilio.twiml.VoiceResponse;
9
10
import javax.inject.Singleton;
11
import javax.servlet.ServletException;
12
import javax.servlet.http.HttpServlet;
13
import javax.servlet.http.HttpServletRequest;
14
import javax.servlet.http.HttpServletResponse;
15
import java.io.IOException;
16
import java.util.logging.Level;
17
import java.util.logging.Logger;
18
19
/**
20
* Returns TwiML instructions to TwilioAppSettings's POST requests
21
*/
22
@Singleton
23
public class IncomingCallServlet extends HttpServlet {
24
25
private static final Logger LOG = Logger.getLogger(IncomingCallServlet.class.getName());
26
27
@Override
28
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
29
IOException {
30
try {
31
final VoiceResponse twimlResponse = new VoiceResponse.Builder()
32
.gather(new Gather.Builder()
33
.action("/call/enqueue")
34
.numDigits(1)
35
.timeout(10)
36
.method(Method.POST)
37
.say(new Say
38
.Builder("For Programmable SMS, press one. For Voice, press any other key.")
39
.build()
40
)
41
.build()
42
).build();
43
44
resp.setContentType("application/xml");
45
resp.getWriter().print(twimlResponse.toXml());
46
} catch (TwiMLException e) {
47
LOG.log(Level.SEVERE, "Unexpected error while creating incoming call response", e);
48
throw new RuntimeException(e);
49
}
50
}
51
52
}

We just asked the caller to choose a product, next we will use their choice to create the appropriate Task.


This is the endpoint set as the action URL on the Gather verb on the previous step. A request is made to this endpoint when the user presses a key during the call. This request has a Digits parameter that holds the pressed keys. A Task will be created based on the pressed digit with the selected_product as an attribute. The Workflow will take this Task's attributes and make a match with the configured expressions in order to find a corresponding Task Queue, so an appropriate available Worker can be assigned to handle it.

We use the Enqueue verb with a workflowSid attribute to integrate with TaskRouter. Then the voice call will be put on hold while TaskRouter tries to find an available Worker to handle this Task.

src/main/java/com/twilio/taskrouter/application/servlet/EnqueueServlet.java

1
package com.twilio.taskrouter.application.servlet;
2
3
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
4
import com.twilio.twiml.EnqueueTask;
5
import com.twilio.twiml.Task;
6
import com.twilio.twiml.TwiMLException;
7
import com.twilio.twiml.VoiceResponse;
8
9
import javax.inject.Inject;
10
import javax.inject.Singleton;
11
import javax.servlet.ServletException;
12
import javax.servlet.http.HttpServlet;
13
import javax.servlet.http.HttpServletRequest;
14
import javax.servlet.http.HttpServletResponse;
15
import java.io.IOException;
16
import java.util.Optional;
17
import java.util.logging.Level;
18
import java.util.logging.Logger;
19
20
import static java.lang.String.format;
21
22
/**
23
* Selects a product by creating a Task on the TaskRouter Workflow
24
*/
25
@Singleton
26
public class EnqueueServlet extends HttpServlet {
27
28
private static final Logger LOG = Logger.getLogger(EnqueueServlet.class.getName());
29
30
private final String workflowSid;
31
32
@Inject
33
public EnqueueServlet(TwilioAppSettings twilioSettings) {
34
this.workflowSid = twilioSettings.getWorkflowSid();
35
}
36
37
@Override
38
public void doPost(HttpServletRequest req, HttpServletResponse resp)
39
throws ServletException, IOException {
40
41
String selectedProduct = getSelectedProduct(req);
42
Task task = new Task.Builder()
43
.data(format("{\"selected_product\": \"%s\"}", selectedProduct))
44
.build();
45
46
EnqueueTask enqueueTask = new EnqueueTask.Builder(task).workflowSid(workflowSid).build();
47
48
VoiceResponse voiceResponse = new VoiceResponse.Builder().enqueue(enqueueTask).build();
49
resp.setContentType("application/xml");
50
try {
51
resp.getWriter().print(voiceResponse.toXml());
52
} catch (TwiMLException e) {
53
LOG.log(Level.SEVERE, e.getMessage(), e);
54
throw new RuntimeException(e);
55
}
56
}
57
58
private String getSelectedProduct(HttpServletRequest request) {
59
return Optional.ofNullable(request.getParameter("Digits"))
60
.filter(x -> x.equals("1")).map((first) -> "ProgrammableSMS").orElse("ProgrammableVoice");
61
}
62
}

After sending a Task to Twilio, let's see how we tell TaskRouter which Worker to use to execute that task.


When TaskRouter selects a Worker, it does the following:

  1. The Task's Assignment Status is set to reserved
  2. A Reservation instance is generated, linking the Task to the selected Worker
  3. At the same time the Reservation is created, a POST request is made to the Workflow's AssignmentCallbackURL, which was configured using the Gradle task createWorkspace(link takes you to an external page)

This request includes the full details of the Task, the selected Worker, and the Reservation.

Handling this Assignment Callback is a key component of building a TaskRouter application as we can instruct how the Worker will handle a Task. We could send a text, email, push notifications or make a call.

Since we created this Task during a voice call with an Enqueue verb, lets instruct TaskRouter to dequeue the call and dial a Worker. If we do not specify a to parameter with a phone number, TaskRouter will pick the Worker's contact_uri attribute.

We also send a post_work_activity_sid which will tell TaskRouter which Activity to assign this worker after the call ends.

src/main/java/com/twilio/taskrouter/application/servlet/AssignmentServlet.java

1
package com.twilio.taskrouter.application.servlet;
2
3
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
4
5
import javax.inject.Inject;
6
import javax.inject.Singleton;
7
import javax.json.Json;
8
import javax.servlet.ServletException;
9
import javax.servlet.http.HttpServlet;
10
import javax.servlet.http.HttpServletRequest;
11
import javax.servlet.http.HttpServletResponse;
12
import java.io.IOException;
13
14
/**
15
* Servlet for Task assignments
16
*/
17
@Singleton
18
public class AssignmentServlet extends HttpServlet {
19
20
private final String dequeueInstruction;
21
22
@Inject
23
public AssignmentServlet(TwilioAppSettings twilioAppSettings) {
24
dequeueInstruction = Json.createObjectBuilder()
25
.add("instruction", "dequeue")
26
.add("post_work_activity_sid", twilioAppSettings.getPostWorkActivitySid())
27
.build().toString();
28
}
29
30
@Override
31
public void doPost(HttpServletRequest req, HttpServletResponse resp)
32
throws ServletException, IOException {
33
resp.setContentType("application/json");
34
resp.getWriter().print(dequeueInstruction);
35
}
36
}

Now that our Tasks are routed properly, let's deal with missed calls in the next step.


This endpoint will be called after each TaskRouter Event is triggered. In our application, we are trying to collect missed calls, so we would like to handle the workflow.timeout event. This event is triggered when the Task waits more than the limit set on Workflow Configuration-- or rather when no worker is available.

Here we use TwilioRestClient to route this call to a Voicemail Twimlet. Twimlets are tiny web applications for voice. This one will generate a TwiML response using Say verb and record a message using Record verb. The recorded message will then be transcribed and sent to the email address configured.

We are also listening for task.canceled. This is triggered when the customer hangs up before being assigned to an agent, therefore canceling the task. Capturing this event allows us to collect the information from the customers that hang up before the Workflow times out.

src/main/java/com/twilio/taskrouter/application/servlet/EventsServlet.java

1
package com.twilio.taskrouter.application.servlet;
2
3
import com.google.inject.persist.Transactional;
4
import com.twilio.rest.api.v2010.account.MessageCreator;
5
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
6
import com.twilio.taskrouter.domain.model.MissedCall;
7
import com.twilio.taskrouter.domain.repository.MissedCallRepository;
8
import com.twilio.type.PhoneNumber;
9
10
import javax.inject.Inject;
11
import javax.inject.Singleton;
12
import javax.json.Json;
13
import javax.json.JsonObject;
14
import javax.servlet.ServletException;
15
import javax.servlet.http.HttpServlet;
16
import javax.servlet.http.HttpServletRequest;
17
import javax.servlet.http.HttpServletResponse;
18
import java.io.IOException;
19
import java.io.StringReader;
20
import java.util.Optional;
21
import java.util.logging.Logger;
22
23
/**
24
* Servlet for Events callback for missed calls
25
*/
26
@Singleton
27
public class EventsServlet extends HttpServlet {
28
29
private static final String LEAVE_MSG = "Sorry, All agents are busy. Please leave a message. "
30
+ "We will call you as soon as possible";
31
32
private static final String OFFLINE_MSG = "Your status has changed to Offline. "
33
+ "Reply with \"On\" to get back Online";
34
35
private static final Logger LOG = Logger.getLogger(EventsServlet.class.getName());
36
37
private final TwilioAppSettings twilioSettings;
38
39
private final MissedCallRepository missedCallRepository;
40
41
@Inject
42
public EventsServlet(TwilioAppSettings twilioSettings,
43
MissedCallRepository missedCallRepository) {
44
this.twilioSettings = twilioSettings;
45
this.missedCallRepository = missedCallRepository;
46
}
47
48
@Override
49
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
50
IOException {
51
Optional.ofNullable(req.getParameter("EventType"))
52
.ifPresent(eventName -> {
53
switch (eventName) {
54
case "workflow.timeout":
55
case "task.canceled":
56
parseAttributes("TaskAttributes", req)
57
.ifPresent(this::addMissingCallAndLeaveMessage);
58
break;
59
case "worker.activity.update":
60
Optional.ofNullable(req.getParameter("WorkerActivityName"))
61
.filter("Offline"::equals)
62
.ifPresent(offlineEvent -> {
63
parseAttributes("WorkerAttributes", req)
64
.ifPresent(this::notifyOfflineStatusToWorker);
65
});
66
break;
67
default:
68
}
69
});
70
}
71
72
private Optional<JsonObject> parseAttributes(String parameter, HttpServletRequest request) {
73
return Optional.ofNullable(request.getParameter(parameter))
74
.map(jsonRequest -> Json.createReader(new StringReader(jsonRequest)).readObject());
75
}
76
77
@Transactional
78
private void addMissingCallAndLeaveMessage(JsonObject taskAttributesJson) {
79
String phoneNumber = taskAttributesJson.getString("from");
80
String selectedProduct = taskAttributesJson.getString("selected_product");
81
82
MissedCall missedCall = new MissedCall(phoneNumber, selectedProduct);
83
missedCallRepository.add(missedCall);
84
LOG.info("Added Missing Call: " + missedCall);
85
86
String callSid = taskAttributesJson.getString("call_sid");
87
twilioSettings.redirectToVoiceMail(callSid, LEAVE_MSG);
88
}
89
90
private void notifyOfflineStatusToWorker(JsonObject workerAttributesJson) {
91
String workerPhone = workerAttributesJson.getString("contact_uri");
92
new MessageCreator(
93
new PhoneNumber(workerPhone),
94
new PhoneNumber(twilioSettings.getPhoneNumber().toString()),
95
OFFLINE_MSG
96
).create();
97
98
}
99
100
}

Most of the features of our application are implemented. The last piece is allowing the Workers to change their availability status. Let's see how to do that next.


Change a Worker's Activity

change-a-workers-activity page anchor

We have created this endpoint, so a worker can send an SMS message to the support line with the command "On" or "Off" to change their availability status.

This is important as a worker's activity will change to Offline when they miss a call. When this happens, they receive an SMS letting them know that their activity has changed, and that they can reply with the On command to make themselves available for incoming calls again.

Change a Worker's Activity

change-a-workers-activity-1 page anchor

src/main/java/com/twilio/taskrouter/application/servlet/MessageServlet.java

1
package com.twilio.taskrouter.application.servlet;
2
3
import com.google.inject.Singleton;
4
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
5
import com.twilio.twiml.Sms;
6
import com.twilio.twiml.TwiMLException;
7
import com.twilio.twiml.VoiceResponse;
8
9
import javax.inject.Inject;
10
import javax.servlet.ServletException;
11
import javax.servlet.http.HttpServlet;
12
import javax.servlet.http.HttpServletRequest;
13
import javax.servlet.http.HttpServletResponse;
14
import java.io.IOException;
15
import java.util.Optional;
16
import java.util.logging.Level;
17
import java.util.logging.Logger;
18
19
/**
20
* Handles the messages sent by workers for activate/deactivate
21
* themselves for receiving calls from users
22
*/
23
@Singleton
24
public class MessageServlet extends HttpServlet {
25
26
private static final Logger LOG = Logger.getLogger(MessageServlet.class.getName());
27
28
private final WorkspaceFacade workspace;
29
30
@Inject
31
public MessageServlet(WorkspaceFacade workspace) {
32
this.workspace = workspace;
33
}
34
35
@Override
36
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
37
throws ServletException, IOException {
38
final VoiceResponse twimlResponse;
39
final String newStatus = getNewWorkerStatus(req);
40
final String workerPhone = req.getParameter("From");
41
42
try {
43
Sms responseSms = workspace.findWorkerByPhone(workerPhone).map(worker -> {
44
workspace.updateWorkerStatus(worker, newStatus);
45
return new Sms.Builder(String.format("Your status has changed to %s", newStatus)).build();
46
}).orElseGet(() -> new Sms.Builder("You are not a valid worker").build());
47
48
twimlResponse = new VoiceResponse.Builder().sms(responseSms).build();
49
resp.setContentType("application/xml");
50
resp.getWriter().print(twimlResponse.toXml());
51
52
} catch (TwiMLException e) {
53
LOG.log(Level.SEVERE, "Error while providing answer to a workers' sms", e);
54
}
55
56
}
57
58
private String getNewWorkerStatus(HttpServletRequest request) {
59
return Optional.ofNullable(request.getParameter("Body"))
60
.filter(x -> x.equals("off")).map((first) -> "Offline").orElse("Idle");
61
}
62
63
}

Congratulations! You finished this tutorial. As you can see, using Twilio's TaskRouter is quite simple.


If you're a Java developer working with Twilio, you might enjoy these other tutorials:

Click to Call

An example application implementing Click to Call using Twilio.

Automated-Survey(link takes you to an external page)

Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.