Skip to contentSkip to navigationSkip to topbar
Rate this page:
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


_71
{
_71
"name": "Twilio Workspace",
_71
"event_callback": "%(host)s/events",
_71
"workers": [
_71
{
_71
"name": "Bob",
_71
"attributes": {
_71
"products": [
_71
"ProgrammableSMS"
_71
],
_71
"contact_uri": "%(bob_number)s"
_71
}
_71
},
_71
{
_71
"name": "Alice",
_71
"attributes": {
_71
"products": [
_71
"ProgrammableVoice"
_71
],
_71
"contact_uri": "%(alice_number)s"
_71
}
_71
}
_71
],
_71
"activities": [
_71
{
_71
"name": "Offline",
_71
"availability": "false"
_71
},
_71
{
_71
"name": "Idle",
_71
"availability": "true"
_71
},
_71
{
_71
"name": "Busy",
_71
"availability": "false"
_71
},
_71
{
_71
"name": "Reserved",
_71
"availability": "false"
_71
}
_71
],
_71
"task_queues": [
_71
{
_71
"name": "Default",
_71
"targetWorkers": "1==1"
_71
},
_71
{
_71
"name": "SMS",
_71
"targetWorkers": "products HAS \"ProgrammableSMS\""
_71
},
_71
{
_71
"name": "Voice",
_71
"targetWorkers": "products HAS \"ProgrammableVoice\""
_71
}
_71
],
_71
"workflow": {
_71
"name": "Sales",
_71
"callback": "%(host)s/assignment",
_71
"timeout": "15",
_71
"routingConfiguration": [
_71
{
_71
"expression": "selected_product==\"ProgrammableSMS\"",
_71
"targetTaskQueue": "SMS"
_71
},
_71
{
_71
"expression": "selected_product==\"ProgrammableVoice\"",
_71
"targetTaskQueue": "Voice"
_71
}
_71
]
_71
}
_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


_265
package com.twilio.taskrouter.application;
_265
_265
import com.google.inject.Guice;
_265
import com.google.inject.Injector;
_265
import com.twilio.rest.taskrouter.v1.workspace.Activity;
_265
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
_265
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
_265
import com.twilio.taskrouter.WorkflowRule;
_265
import com.twilio.taskrouter.WorkflowRuleTarget;
_265
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_265
import com.twilio.taskrouter.domain.common.Utils;
_265
import com.twilio.taskrouter.domain.error.TaskRouterException;
_265
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
_265
import org.apache.commons.lang3.StringUtils;
_265
import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
import javax.json.Json;
_265
import javax.json.JsonArray;
_265
import javax.json.JsonObject;
_265
import javax.json.JsonReader;
_265
import java.io.File;
_265
import java.io.IOException;
_265
import java.io.StringReader;
_265
import java.net.URISyntaxException;
_265
import java.net.URL;
_265
import java.util.Arrays;
_265
import java.util.HashMap;
_265
import java.util.List;
_265
import java.util.Map;
_265
import java.util.Optional;
_265
import java.util.Properties;
_265
import java.util.logging.Logger;
_265
import java.util.stream.Collectors;
_265
_265
import static java.lang.System.exit;
_265
_265
//import org.apache.commons.lang3.StringUtils;
_265
//import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
/**
_265
* Creates a workspace
_265
*/
_265
class CreateWorkspaceTask {
_265
_265
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
_265
_265
public static void main(String[] args) {
_265
_265
System.out.println("Creating workspace...");
_265
if (args.length < 3) {
_265
System.out.println("You must specify 3 parameters:");
_265
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
_265
System.out.println("- Phone of the first agent (Bob)");
_265
System.out.println("- Phone of the secondary agent (Alice)");
_265
exit(1);
_265
}
_265
_265
String hostname = args[0];
_265
String bobPhone = args[1];
_265
String alicePhone = args[2];
_265
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
_265
hostname, bobPhone, alicePhone));
_265
_265
//Get the configuration
_265
JsonObject workspaceConfig = createWorkspaceConfig(args);
_265
_265
//Get or Create the Workspace
_265
Injector injector = Guice.createInjector();
_265
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
_265
_265
String workspaceName = workspaceConfig.getString("name");
_265
Map<String, String> workspaceParams = new HashMap<>();
_265
workspaceParams.put("FriendlyName", workspaceName);
_265
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
_265
_265
try {
_265
WorkspaceFacade workspaceFacade = WorkspaceFacade
_265
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
_265
_265
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
_265
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
_265
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
_265
_265
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
_265
} catch (TaskRouterException e) {
_265
LOG.severe(e.getMessage());
_265
exit(1);
_265
}
_265
}
_265
_265
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
_265
Map<String, String> workerParams = new HashMap<>();
_265
workerParams.put("FriendlyName", workerJson.getString("name"));
_265
workerParams.put("ActivitySid", idleActivity.getSid());
_265
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
_265
_265
try {
_265
workspaceFacade.addWorker(workerParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
_265
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
_265
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
_265
new TaskRouterException("The activity for assignments 'Busy' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
_265
Map<String, String> taskQueueParams = new HashMap<>();
_265
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
_265
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
_265
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
_265
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
_265
_265
try {
_265
workspaceFacade.addTaskQueue(taskQueueParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceConfig) {
_265
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
_265
String workflowName = workflowJson.getString("name");
_265
return workspaceFacade.findWorkflowByName(workflowName)
_265
.orElseGet(() -> {
_265
Map<String, String> workflowParams = new HashMap<>();
_265
workflowParams.put("FriendlyName", workflowName);
_265
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
_265
_265
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
_265
workflowParams.put("Configuration", workflowConfigJson);
_265
_265
return workspaceFacade.addWorkflow(workflowParams);
_265
});
_265
}
_265
_265
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
_265
Workflow workflow,
_265
TwilioAppSettings twilioSettings) {
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
Properties workspaceParams = new Properties();
_265
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
_265
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
_265
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
_265
workspaceParams.put("workflow.sid", workflow.getSid());
_265
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
_265
workspaceParams.put("email", twilioSettings.getEmail());
_265
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
_265
_265
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
_265
_265
try {
_265
Utils.saveProperties(workspaceParams,
_265
workspacePropertiesFile,
_265
"Properties for last created Twilio TaskRouter workspace");
_265
} catch (IOException e) {
_265
LOG.severe("Could not save workspace.properties with current configuration");
_265
exit(1);
_265
}
_265
_265
String successMsg = String.format("Workspace '%s' was created successfully.",
_265
workspaceFacade.getFriendlyName());
_265
final int lineLength = successMsg.length() + 2;
_265
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println(String.format(" %s ", successMsg));
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println("The following variables were registered:");
_265
System.out.println("\n");
_265
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
_265
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
_265
});
_265
System.out.println("\n");
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
}
_265
_265
public static JsonObject createWorkspaceConfig(String[] args) {
_265
final String configFileName = "workspace.json";
_265
_265
Optional<URL> url =
_265
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
_265
return url.map(u -> {
_265
try {
_265
File workspaceConfigJsonFile = new File(u.toURI());
_265
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
_265
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
_265
_265
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
_265
return jsonReader.readObject();
_265
}
_265
} catch (URISyntaxException e) {
_265
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
_265
configFileName, e.getMessage()));
_265
} catch (IOException e) {
_265
throw new TaskRouterException(String.format("Error while reading %s: %s",
_265
configFileName, e.getMessage()));
_265
}
_265
}).orElseThrow(
_265
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
_265
}
_265
_265
private static String parseWorkspaceJsonContent(final String unparsedContent,
_265
final String... args) {
_265
Map<String, String> values = new HashMap<>();
_265
values.put("host", args[0]);
_265
values.put("bob_number", args[1]);
_265
values.put("alice_number", args[2]);
_265
_265
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
_265
return strSubstitutor.replace(unparsedContent);
_265
}
_265
_265
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
_265
JsonObject workflowJson) {
_265
try {
_265
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
_265
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
_265
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
_265
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
_265
.expression("1=1")
_265
.priority(1)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
_265
.map(ruleJson -> {
_265
String ruleQueueName = ruleJson.getString("targetTaskQueue");
_265
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
_265
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
_265
_265
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
_265
.priority(5)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
_265
_265
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
_265
}).collect(Collectors.toList());
_265
_265
com.twilio.taskrouter.Workflow config;
_265
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
_265
return config.toJson();
_265
} catch (Exception ex) {
_265
throw new TaskRouterException("Error while creating workflow json configuration", ex);
_265
}
_265
}
_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


_166
package com.twilio.taskrouter.domain.model;
_166
_166
import com.fasterxml.jackson.databind.ObjectMapper;
_166
import com.twilio.base.ResourceSet;
_166
import com.twilio.http.TwilioRestClient;
_166
import com.twilio.rest.taskrouter.v1.Workspace;
_166
import com.twilio.rest.taskrouter.v1.WorkspaceCreator;
_166
import com.twilio.rest.taskrouter.v1.WorkspaceDeleter;
_166
import com.twilio.rest.taskrouter.v1.WorkspaceFetcher;
_166
import com.twilio.rest.taskrouter.v1.WorkspaceReader;
_166
import com.twilio.rest.taskrouter.v1.workspace.Activity;
_166
import com.twilio.rest.taskrouter.v1.workspace.ActivityReader;
_166
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
_166
import com.twilio.rest.taskrouter.v1.workspace.TaskQueueCreator;
_166
import com.twilio.rest.taskrouter.v1.workspace.TaskQueueReader;
_166
import com.twilio.rest.taskrouter.v1.workspace.Worker;
_166
import com.twilio.rest.taskrouter.v1.workspace.WorkerCreator;
_166
import com.twilio.rest.taskrouter.v1.workspace.WorkerReader;
_166
import com.twilio.rest.taskrouter.v1.workspace.WorkerUpdater;
_166
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
_166
import com.twilio.rest.taskrouter.v1.workspace.WorkflowCreator;
_166
import com.twilio.rest.taskrouter.v1.workspace.WorkflowReader;
_166
import com.twilio.taskrouter.domain.error.TaskRouterException;
_166
_166
import java.io.IOException;
_166
import java.util.HashMap;
_166
import java.util.Map;
_166
import java.util.Optional;
_166
import java.util.stream.StreamSupport;
_166
_166
public class WorkspaceFacade {
_166
_166
private final TwilioRestClient client;
_166
_166
private final Workspace workspace;
_166
_166
private Activity idleActivity;
_166
_166
private Map<String, Worker> phoneToWorker;
_166
_166
public WorkspaceFacade(TwilioRestClient client, Workspace workspace) {
_166
this.client = client;
_166
this.workspace = workspace;
_166
}
_166
_166
public static WorkspaceFacade create(TwilioRestClient client,
_166
Map<String, String> params) {
_166
String workspaceName = params.get("FriendlyName");
_166
String eventCallbackUrl = params.get("EventCallbackUrl");
_166
_166
ResourceSet<Workspace> execute = new WorkspaceReader()
_166
.setFriendlyName(workspaceName)
_166
.read(client);
_166
StreamSupport.stream(execute.spliterator(), false)
_166
.findFirst()
_166
.ifPresent(workspace -> new WorkspaceDeleter(workspace.getSid()).delete(client));
_166
_166
Workspace workspace = new WorkspaceCreator(workspaceName)
_166
.setEventCallbackUrl(eventCallbackUrl)
_166
.create(client);
_166
_166
return new WorkspaceFacade(client, workspace);
_166
}
_166
_166
public static Optional<WorkspaceFacade> findBySid(String workspaceSid,
_166
TwilioRestClient client) {
_166
Workspace workspace = new WorkspaceFetcher(workspaceSid).fetch(client);
_166
return Optional.of(new WorkspaceFacade(client, workspace));
_166
}
_166
_166
public String getFriendlyName() {
_166
return workspace.getFriendlyName();
_166
}
_166
_166
public String getSid() {
_166
return workspace.getSid();
_166
}
_166
_166
public Worker addWorker(Map<String, String> workerParams) {
_166
return new WorkerCreator(workspace.getSid(), workerParams.get("FriendlyName"))
_166
.setActivitySid(workerParams.get("ActivitySid"))
_166
.setAttributes(workerParams.get("Attributes"))
_166
.create(client);
_166
}
_166
_166
public void addTaskQueue(Map<String, String> taskQueueParams) {
_166
new TaskQueueCreator(this.workspace.getSid(),
_166
taskQueueParams.get("FriendlyName"),
_166
taskQueueParams.get("ReservationActivitySid"),
_166
taskQueueParams.get("AssignmentActivitySid"))
_166
.create(client);
_166
}
_166
_166
public Workflow addWorkflow(Map<String, String> workflowParams) {
_166
return new WorkflowCreator(workspace.getSid(),
_166
workflowParams.get("FriendlyName"),
_166
workflowParams.get("Configuration"))
_166
.setAssignmentCallbackUrl(workflowParams.get("AssignmentCallbackUrl"))
_166
.setFallbackAssignmentCallbackUrl(workflowParams.get("FallbackAssignmentCallbackUrl"))
_166
.setTaskReservationTimeout(Integer.valueOf(workflowParams.get("TaskReservationTimeout")))
_166
.create(client);
_166
}
_166
_166
public Optional<Activity> findActivityByName(String activityName) {
_166
return StreamSupport.stream(new ActivityReader(this.workspace.getSid())
_166
.setFriendlyName(activityName)
_166
.read(client).spliterator(), false
_166
).findFirst();
_166
}
_166
_166
public Optional<TaskQueue> findTaskQueueByName(String queueName) {
_166
return StreamSupport.stream(new TaskQueueReader(this.workspace.getSid())
_166
.setFriendlyName(queueName)
_166
.read(client).spliterator(), false
_166
).findFirst();
_166
}
_166
_166
public Optional<Workflow> findWorkflowByName(String workflowName) {
_166
return StreamSupport.stream(new WorkflowReader(this.workspace.getSid())
_166
.setFriendlyName(workflowName)
_166
.read(client).spliterator(), false
_166
).findFirst();
_166
}
_166
_166
public Optional<Worker> findWorkerByPhone(String workerPhone) {
_166
return Optional.ofNullable(getPhoneToWorker().get(workerPhone));
_166
}
_166
_166
public Map<String, Worker> getPhoneToWorker() {
_166
if (phoneToWorker == null) {
_166
phoneToWorker = new HashMap<>();
_166
StreamSupport.stream(
_166
new WorkerReader(this.workspace.getSid()).read(client).spliterator(), false
_166
).forEach(worker -> {
_166
try {
_166
HashMap<String, Object> attributes = new ObjectMapper()
_166
.readValue(worker.getAttributes(), HashMap.class);
_166
phoneToWorker.put(attributes.get("contact_uri").toString(), worker);
_166
} catch (IOException e) {
_166
throw new TaskRouterException(
_166
String.format("'%s' has a malformed json attributes", worker.getFriendlyName()));
_166
}
_166
});
_166
}
_166
return phoneToWorker;
_166
}
_166
_166
public Activity getIdleActivity() {
_166
if (idleActivity == null) {
_166
idleActivity = findActivityByName("Idle").get();
_166
}
_166
return idleActivity;
_166
}
_166
_166
public void updateWorkerStatus(Worker worker, String activityFriendlyName) {
_166
Activity activity = findActivityByName(activityFriendlyName).orElseThrow(() ->
_166
new TaskRouterException(
_166
String.format("The activity '%s' doesn't exist in the workspace", activityFriendlyName)
_166
)
_166
);
_166
_166
new WorkerUpdater(workspace.getSid(), worker.getSid())
_166
.setActivitySid(activity.getSid())
_166
.update(client);
_166
}
_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


_265
package com.twilio.taskrouter.application;
_265
_265
import com.google.inject.Guice;
_265
import com.google.inject.Injector;
_265
import com.twilio.rest.taskrouter.v1.workspace.Activity;
_265
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
_265
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
_265
import com.twilio.taskrouter.WorkflowRule;
_265
import com.twilio.taskrouter.WorkflowRuleTarget;
_265
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_265
import com.twilio.taskrouter.domain.common.Utils;
_265
import com.twilio.taskrouter.domain.error.TaskRouterException;
_265
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
_265
import org.apache.commons.lang3.StringUtils;
_265
import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
import javax.json.Json;
_265
import javax.json.JsonArray;
_265
import javax.json.JsonObject;
_265
import javax.json.JsonReader;
_265
import java.io.File;
_265
import java.io.IOException;
_265
import java.io.StringReader;
_265
import java.net.URISyntaxException;
_265
import java.net.URL;
_265
import java.util.Arrays;
_265
import java.util.HashMap;
_265
import java.util.List;
_265
import java.util.Map;
_265
import java.util.Optional;
_265
import java.util.Properties;
_265
import java.util.logging.Logger;
_265
import java.util.stream.Collectors;
_265
_265
import static java.lang.System.exit;
_265
_265
//import org.apache.commons.lang3.StringUtils;
_265
//import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
/**
_265
* Creates a workspace
_265
*/
_265
class CreateWorkspaceTask {
_265
_265
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
_265
_265
public static void main(String[] args) {
_265
_265
System.out.println("Creating workspace...");
_265
if (args.length < 3) {
_265
System.out.println("You must specify 3 parameters:");
_265
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
_265
System.out.println("- Phone of the first agent (Bob)");
_265
System.out.println("- Phone of the secondary agent (Alice)");
_265
exit(1);
_265
}
_265
_265
String hostname = args[0];
_265
String bobPhone = args[1];
_265
String alicePhone = args[2];
_265
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
_265
hostname, bobPhone, alicePhone));
_265
_265
//Get the configuration
_265
JsonObject workspaceConfig = createWorkspaceConfig(args);
_265
_265
//Get or Create the Workspace
_265
Injector injector = Guice.createInjector();
_265
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
_265
_265
String workspaceName = workspaceConfig.getString("name");
_265
Map<String, String> workspaceParams = new HashMap<>();
_265
workspaceParams.put("FriendlyName", workspaceName);
_265
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
_265
_265
try {
_265
WorkspaceFacade workspaceFacade = WorkspaceFacade
_265
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
_265
_265
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
_265
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
_265
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
_265
_265
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
_265
} catch (TaskRouterException e) {
_265
LOG.severe(e.getMessage());
_265
exit(1);
_265
}
_265
}
_265
_265
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
_265
Map<String, String> workerParams = new HashMap<>();
_265
workerParams.put("FriendlyName", workerJson.getString("name"));
_265
workerParams.put("ActivitySid", idleActivity.getSid());
_265
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
_265
_265
try {
_265
workspaceFacade.addWorker(workerParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
_265
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
_265
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
_265
new TaskRouterException("The activity for assignments 'Busy' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
_265
Map<String, String> taskQueueParams = new HashMap<>();
_265
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
_265
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
_265
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
_265
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
_265
_265
try {
_265
workspaceFacade.addTaskQueue(taskQueueParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceConfig) {
_265
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
_265
String workflowName = workflowJson.getString("name");
_265
return workspaceFacade.findWorkflowByName(workflowName)
_265
.orElseGet(() -> {
_265
Map<String, String> workflowParams = new HashMap<>();
_265
workflowParams.put("FriendlyName", workflowName);
_265
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
_265
_265
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
_265
workflowParams.put("Configuration", workflowConfigJson);
_265
_265
return workspaceFacade.addWorkflow(workflowParams);
_265
});
_265
}
_265
_265
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
_265
Workflow workflow,
_265
TwilioAppSettings twilioSettings) {
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
Properties workspaceParams = new Properties();
_265
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
_265
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
_265
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
_265
workspaceParams.put("workflow.sid", workflow.getSid());
_265
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
_265
workspaceParams.put("email", twilioSettings.getEmail());
_265
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
_265
_265
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
_265
_265
try {
_265
Utils.saveProperties(workspaceParams,
_265
workspacePropertiesFile,
_265
"Properties for last created Twilio TaskRouter workspace");
_265
} catch (IOException e) {
_265
LOG.severe("Could not save workspace.properties with current configuration");
_265
exit(1);
_265
}
_265
_265
String successMsg = String.format("Workspace '%s' was created successfully.",
_265
workspaceFacade.getFriendlyName());
_265
final int lineLength = successMsg.length() + 2;
_265
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println(String.format(" %s ", successMsg));
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println("The following variables were registered:");
_265
System.out.println("\n");
_265
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
_265
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
_265
});
_265
System.out.println("\n");
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
}
_265
_265
public static JsonObject createWorkspaceConfig(String[] args) {
_265
final String configFileName = "workspace.json";
_265
_265
Optional<URL> url =
_265
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
_265
return url.map(u -> {
_265
try {
_265
File workspaceConfigJsonFile = new File(u.toURI());
_265
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
_265
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
_265
_265
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
_265
return jsonReader.readObject();
_265
}
_265
} catch (URISyntaxException e) {
_265
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
_265
configFileName, e.getMessage()));
_265
} catch (IOException e) {
_265
throw new TaskRouterException(String.format("Error while reading %s: %s",
_265
configFileName, e.getMessage()));
_265
}
_265
}).orElseThrow(
_265
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
_265
}
_265
_265
private static String parseWorkspaceJsonContent(final String unparsedContent,
_265
final String... args) {
_265
Map<String, String> values = new HashMap<>();
_265
values.put("host", args[0]);
_265
values.put("bob_number", args[1]);
_265
values.put("alice_number", args[2]);
_265
_265
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
_265
return strSubstitutor.replace(unparsedContent);
_265
}
_265
_265
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
_265
JsonObject workflowJson) {
_265
try {
_265
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
_265
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
_265
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
_265
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
_265
.expression("1=1")
_265
.priority(1)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
_265
.map(ruleJson -> {
_265
String ruleQueueName = ruleJson.getString("targetTaskQueue");
_265
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
_265
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
_265
_265
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
_265
.priority(5)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
_265
_265
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
_265
}).collect(Collectors.toList());
_265
_265
com.twilio.taskrouter.Workflow config;
_265
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
_265
return config.toJson();
_265
} catch (Exception ex) {
_265
throw new TaskRouterException("Error while creating workflow json configuration", ex);
_265
}
_265
}
_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


_265
package com.twilio.taskrouter.application;
_265
_265
import com.google.inject.Guice;
_265
import com.google.inject.Injector;
_265
import com.twilio.rest.taskrouter.v1.workspace.Activity;
_265
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
_265
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
_265
import com.twilio.taskrouter.WorkflowRule;
_265
import com.twilio.taskrouter.WorkflowRuleTarget;
_265
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_265
import com.twilio.taskrouter.domain.common.Utils;
_265
import com.twilio.taskrouter.domain.error.TaskRouterException;
_265
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
_265
import org.apache.commons.lang3.StringUtils;
_265
import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
import javax.json.Json;
_265
import javax.json.JsonArray;
_265
import javax.json.JsonObject;
_265
import javax.json.JsonReader;
_265
import java.io.File;
_265
import java.io.IOException;
_265
import java.io.StringReader;
_265
import java.net.URISyntaxException;
_265
import java.net.URL;
_265
import java.util.Arrays;
_265
import java.util.HashMap;
_265
import java.util.List;
_265
import java.util.Map;
_265
import java.util.Optional;
_265
import java.util.Properties;
_265
import java.util.logging.Logger;
_265
import java.util.stream.Collectors;
_265
_265
import static java.lang.System.exit;
_265
_265
//import org.apache.commons.lang3.StringUtils;
_265
//import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
/**
_265
* Creates a workspace
_265
*/
_265
class CreateWorkspaceTask {
_265
_265
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
_265
_265
public static void main(String[] args) {
_265
_265
System.out.println("Creating workspace...");
_265
if (args.length < 3) {
_265
System.out.println("You must specify 3 parameters:");
_265
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
_265
System.out.println("- Phone of the first agent (Bob)");
_265
System.out.println("- Phone of the secondary agent (Alice)");
_265
exit(1);
_265
}
_265
_265
String hostname = args[0];
_265
String bobPhone = args[1];
_265
String alicePhone = args[2];
_265
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
_265
hostname, bobPhone, alicePhone));
_265
_265
//Get the configuration
_265
JsonObject workspaceConfig = createWorkspaceConfig(args);
_265
_265
//Get or Create the Workspace
_265
Injector injector = Guice.createInjector();
_265
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
_265
_265
String workspaceName = workspaceConfig.getString("name");
_265
Map<String, String> workspaceParams = new HashMap<>();
_265
workspaceParams.put("FriendlyName", workspaceName);
_265
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
_265
_265
try {
_265
WorkspaceFacade workspaceFacade = WorkspaceFacade
_265
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
_265
_265
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
_265
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
_265
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
_265
_265
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
_265
} catch (TaskRouterException e) {
_265
LOG.severe(e.getMessage());
_265
exit(1);
_265
}
_265
}
_265
_265
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
_265
Map<String, String> workerParams = new HashMap<>();
_265
workerParams.put("FriendlyName", workerJson.getString("name"));
_265
workerParams.put("ActivitySid", idleActivity.getSid());
_265
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
_265
_265
try {
_265
workspaceFacade.addWorker(workerParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
_265
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
_265
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
_265
new TaskRouterException("The activity for assignments 'Busy' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
_265
Map<String, String> taskQueueParams = new HashMap<>();
_265
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
_265
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
_265
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
_265
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
_265
_265
try {
_265
workspaceFacade.addTaskQueue(taskQueueParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceConfig) {
_265
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
_265
String workflowName = workflowJson.getString("name");
_265
return workspaceFacade.findWorkflowByName(workflowName)
_265
.orElseGet(() -> {
_265
Map<String, String> workflowParams = new HashMap<>();
_265
workflowParams.put("FriendlyName", workflowName);
_265
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
_265
_265
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
_265
workflowParams.put("Configuration", workflowConfigJson);
_265
_265
return workspaceFacade.addWorkflow(workflowParams);
_265
});
_265
}
_265
_265
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
_265
Workflow workflow,
_265
TwilioAppSettings twilioSettings) {
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
Properties workspaceParams = new Properties();
_265
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
_265
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
_265
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
_265
workspaceParams.put("workflow.sid", workflow.getSid());
_265
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
_265
workspaceParams.put("email", twilioSettings.getEmail());
_265
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
_265
_265
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
_265
_265
try {
_265
Utils.saveProperties(workspaceParams,
_265
workspacePropertiesFile,
_265
"Properties for last created Twilio TaskRouter workspace");
_265
} catch (IOException e) {
_265
LOG.severe("Could not save workspace.properties with current configuration");
_265
exit(1);
_265
}
_265
_265
String successMsg = String.format("Workspace '%s' was created successfully.",
_265
workspaceFacade.getFriendlyName());
_265
final int lineLength = successMsg.length() + 2;
_265
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println(String.format(" %s ", successMsg));
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println("The following variables were registered:");
_265
System.out.println("\n");
_265
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
_265
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
_265
});
_265
System.out.println("\n");
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
}
_265
_265
public static JsonObject createWorkspaceConfig(String[] args) {
_265
final String configFileName = "workspace.json";
_265
_265
Optional<URL> url =
_265
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
_265
return url.map(u -> {
_265
try {
_265
File workspaceConfigJsonFile = new File(u.toURI());
_265
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
_265
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
_265
_265
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
_265
return jsonReader.readObject();
_265
}
_265
} catch (URISyntaxException e) {
_265
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
_265
configFileName, e.getMessage()));
_265
} catch (IOException e) {
_265
throw new TaskRouterException(String.format("Error while reading %s: %s",
_265
configFileName, e.getMessage()));
_265
}
_265
}).orElseThrow(
_265
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
_265
}
_265
_265
private static String parseWorkspaceJsonContent(final String unparsedContent,
_265
final String... args) {
_265
Map<String, String> values = new HashMap<>();
_265
values.put("host", args[0]);
_265
values.put("bob_number", args[1]);
_265
values.put("alice_number", args[2]);
_265
_265
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
_265
return strSubstitutor.replace(unparsedContent);
_265
}
_265
_265
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
_265
JsonObject workflowJson) {
_265
try {
_265
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
_265
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
_265
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
_265
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
_265
.expression("1=1")
_265
.priority(1)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
_265
.map(ruleJson -> {
_265
String ruleQueueName = ruleJson.getString("targetTaskQueue");
_265
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
_265
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
_265
_265
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
_265
.priority(5)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
_265
_265
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
_265
}).collect(Collectors.toList());
_265
_265
com.twilio.taskrouter.Workflow config;
_265
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
_265
return config.toJson();
_265
} catch (Exception ex) {
_265
throw new TaskRouterException("Error while creating workflow json configuration", ex);
_265
}
_265
}
_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


_265
package com.twilio.taskrouter.application;
_265
_265
import com.google.inject.Guice;
_265
import com.google.inject.Injector;
_265
import com.twilio.rest.taskrouter.v1.workspace.Activity;
_265
import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;
_265
import com.twilio.rest.taskrouter.v1.workspace.Workflow;
_265
import com.twilio.taskrouter.WorkflowRule;
_265
import com.twilio.taskrouter.WorkflowRuleTarget;
_265
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_265
import com.twilio.taskrouter.domain.common.Utils;
_265
import com.twilio.taskrouter.domain.error.TaskRouterException;
_265
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
_265
import org.apache.commons.lang3.StringUtils;
_265
import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
import javax.json.Json;
_265
import javax.json.JsonArray;
_265
import javax.json.JsonObject;
_265
import javax.json.JsonReader;
_265
import java.io.File;
_265
import java.io.IOException;
_265
import java.io.StringReader;
_265
import java.net.URISyntaxException;
_265
import java.net.URL;
_265
import java.util.Arrays;
_265
import java.util.HashMap;
_265
import java.util.List;
_265
import java.util.Map;
_265
import java.util.Optional;
_265
import java.util.Properties;
_265
import java.util.logging.Logger;
_265
import java.util.stream.Collectors;
_265
_265
import static java.lang.System.exit;
_265
_265
//import org.apache.commons.lang3.StringUtils;
_265
//import org.apache.commons.lang3.text.StrSubstitutor;
_265
_265
/**
_265
* Creates a workspace
_265
*/
_265
class CreateWorkspaceTask {
_265
_265
private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());
_265
_265
public static void main(String[] args) {
_265
_265
System.out.println("Creating workspace...");
_265
if (args.length < 3) {
_265
System.out.println("You must specify 3 parameters:");
_265
System.out.println("- Server hostname. E.g, <hash>.ngrok.com");
_265
System.out.println("- Phone of the first agent (Bob)");
_265
System.out.println("- Phone of the secondary agent (Alice)");
_265
exit(1);
_265
}
_265
_265
String hostname = args[0];
_265
String bobPhone = args[1];
_265
String alicePhone = args[2];
_265
System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",
_265
hostname, bobPhone, alicePhone));
_265
_265
//Get the configuration
_265
JsonObject workspaceConfig = createWorkspaceConfig(args);
_265
_265
//Get or Create the Workspace
_265
Injector injector = Guice.createInjector();
_265
final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);
_265
_265
String workspaceName = workspaceConfig.getString("name");
_265
Map<String, String> workspaceParams = new HashMap<>();
_265
workspaceParams.put("FriendlyName", workspaceName);
_265
workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));
_265
_265
try {
_265
WorkspaceFacade workspaceFacade = WorkspaceFacade
_265
.create(twilioSettings.getTwilioRestClient(), workspaceParams);
_265
_265
addWorkersToWorkspace(workspaceFacade, workspaceConfig);
_265
addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);
_265
Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);
_265
_265
printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);
_265
} catch (TaskRouterException e) {
_265
LOG.severe(e.getMessage());
_265
exit(1);
_265
}
_265
}
_265
_265
public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {
_265
Map<String, String> workerParams = new HashMap<>();
_265
workerParams.put("FriendlyName", workerJson.getString("name"));
_265
workerParams.put("ActivitySid", idleActivity.getSid());
_265
workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());
_265
_265
try {
_265
workspaceFacade.addWorker(workerParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceJsonConfig) {
_265
JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");
_265
Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->
_265
new TaskRouterException("The activity for reservations 'Reserved' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->
_265
new TaskRouterException("The activity for assignments 'Busy' was not found. "
_265
+ "TaskQueues cannot be added."));
_265
taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {
_265
Map<String, String> taskQueueParams = new HashMap<>();
_265
taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));
_265
taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));
_265
taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());
_265
taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());
_265
_265
try {
_265
workspaceFacade.addTaskQueue(taskQueueParams);
_265
} catch (TaskRouterException e) {
_265
LOG.warning(e.getMessage());
_265
}
_265
});
_265
}
_265
_265
public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,
_265
JsonObject workspaceConfig) {
_265
JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");
_265
String workflowName = workflowJson.getString("name");
_265
return workspaceFacade.findWorkflowByName(workflowName)
_265
.orElseGet(() -> {
_265
Map<String, String> workflowParams = new HashMap<>();
_265
workflowParams.put("FriendlyName", workflowName);
_265
workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));
_265
workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));
_265
_265
String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);
_265
workflowParams.put("Configuration", workflowConfigJson);
_265
_265
return workspaceFacade.addWorkflow(workflowParams);
_265
});
_265
}
_265
_265
public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,
_265
Workflow workflow,
_265
TwilioAppSettings twilioSettings) {
_265
Activity idleActivity = workspaceFacade.getIdleActivity();
_265
_265
Properties workspaceParams = new Properties();
_265
workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());
_265
workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());
_265
workspaceParams.put("workspace.sid", workspaceFacade.getSid());
_265
workspaceParams.put("workflow.sid", workflow.getSid());
_265
workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());
_265
workspaceParams.put("email", twilioSettings.getEmail());
_265
workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());
_265
_265
File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);
_265
_265
try {
_265
Utils.saveProperties(workspaceParams,
_265
workspacePropertiesFile,
_265
"Properties for last created Twilio TaskRouter workspace");
_265
} catch (IOException e) {
_265
LOG.severe("Could not save workspace.properties with current configuration");
_265
exit(1);
_265
}
_265
_265
String successMsg = String.format("Workspace '%s' was created successfully.",
_265
workspaceFacade.getFriendlyName());
_265
final int lineLength = successMsg.length() + 2;
_265
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println(String.format(" %s ", successMsg));
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
System.out.println("The following variables were registered:");
_265
System.out.println("\n");
_265
workspaceParams.entrySet().stream().forEach(propertyEntry -> {
_265
System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));
_265
});
_265
System.out.println("\n");
_265
System.out.println(StringUtils.repeat("#", lineLength));
_265
}
_265
_265
public static JsonObject createWorkspaceConfig(String[] args) {
_265
final String configFileName = "workspace.json";
_265
_265
Optional<URL> url =
_265
Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));
_265
return url.map(u -> {
_265
try {
_265
File workspaceConfigJsonFile = new File(u.toURI());
_265
String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);
_265
String parsedContent = parseWorkspaceJsonContent(jsonContent, args);
_265
_265
try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {
_265
return jsonReader.readObject();
_265
}
_265
} catch (URISyntaxException e) {
_265
throw new TaskRouterException(String.format("Wrong uri to find %s: %s",
_265
configFileName, e.getMessage()));
_265
} catch (IOException e) {
_265
throw new TaskRouterException(String.format("Error while reading %s: %s",
_265
configFileName, e.getMessage()));
_265
}
_265
}).orElseThrow(
_265
() -> new TaskRouterException("There's no valid configuration in " + configFileName));
_265
}
_265
_265
private static String parseWorkspaceJsonContent(final String unparsedContent,
_265
final String... args) {
_265
Map<String, String> values = new HashMap<>();
_265
values.put("host", args[0]);
_265
values.put("bob_number", args[1]);
_265
values.put("alice_number", args[2]);
_265
_265
StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");
_265
return strSubstitutor.replace(unparsedContent);
_265
}
_265
_265
public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,
_265
JsonObject workflowJson) {
_265
try {
_265
JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");
_265
TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")
_265
.orElseThrow(() -> new TaskRouterException("Default queue not found"));
_265
WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())
_265
.expression("1=1")
_265
.priority(1)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()
_265
.map(ruleJson -> {
_265
String ruleQueueName = ruleJson.getString("targetTaskQueue");
_265
TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(
_265
() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));
_265
_265
WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())
_265
.priority(5)
_265
.timeout(30)
_265
.build();
_265
_265
List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);
_265
_265
return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();
_265
}).collect(Collectors.toList());
_265
_265
com.twilio.taskrouter.Workflow config;
_265
config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);
_265
return config.toJson();
_265
} catch (Exception ex) {
_265
throw new TaskRouterException("Error while creating workflow json configuration", ex);
_265
}
_265
}
_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


_52
package com.twilio.taskrouter.application.servlet;
_52
_52
_52
import com.twilio.twiml.Gather;
_52
import com.twilio.twiml.Method;
_52
import com.twilio.twiml.Say;
_52
import com.twilio.twiml.TwiMLException;
_52
import com.twilio.twiml.VoiceResponse;
_52
_52
import javax.inject.Singleton;
_52
import javax.servlet.ServletException;
_52
import javax.servlet.http.HttpServlet;
_52
import javax.servlet.http.HttpServletRequest;
_52
import javax.servlet.http.HttpServletResponse;
_52
import java.io.IOException;
_52
import java.util.logging.Level;
_52
import java.util.logging.Logger;
_52
_52
/**
_52
* Returns TwiML instructions to TwilioAppSettings's POST requests
_52
*/
_52
@Singleton
_52
public class IncomingCallServlet extends HttpServlet {
_52
_52
private static final Logger LOG = Logger.getLogger(IncomingCallServlet.class.getName());
_52
_52
@Override
_52
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
_52
IOException {
_52
try {
_52
final VoiceResponse twimlResponse = new VoiceResponse.Builder()
_52
.gather(new Gather.Builder()
_52
.action("/call/enqueue")
_52
.numDigits(1)
_52
.timeout(10)
_52
.method(Method.POST)
_52
.say(new Say
_52
.Builder("For Programmable SMS, press one. For Voice, press any other key.")
_52
.build()
_52
)
_52
.build()
_52
).build();
_52
_52
resp.setContentType("application/xml");
_52
resp.getWriter().print(twimlResponse.toXml());
_52
} catch (TwiMLException e) {
_52
LOG.log(Level.SEVERE, "Unexpected error while creating incoming call response", e);
_52
throw new RuntimeException(e);
_52
}
_52
}
_52
_52
}

We just asked the caller to choose a product, next we will use their choice to create the appropiate 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


_62
package com.twilio.taskrouter.application.servlet;
_62
_62
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_62
import com.twilio.twiml.EnqueueTask;
_62
import com.twilio.twiml.Task;
_62
import com.twilio.twiml.TwiMLException;
_62
import com.twilio.twiml.VoiceResponse;
_62
_62
import javax.inject.Inject;
_62
import javax.inject.Singleton;
_62
import javax.servlet.ServletException;
_62
import javax.servlet.http.HttpServlet;
_62
import javax.servlet.http.HttpServletRequest;
_62
import javax.servlet.http.HttpServletResponse;
_62
import java.io.IOException;
_62
import java.util.Optional;
_62
import java.util.logging.Level;
_62
import java.util.logging.Logger;
_62
_62
import static java.lang.String.format;
_62
_62
/**
_62
* Selects a product by creating a Task on the TaskRouter Workflow
_62
*/
_62
@Singleton
_62
public class EnqueueServlet extends HttpServlet {
_62
_62
private static final Logger LOG = Logger.getLogger(EnqueueServlet.class.getName());
_62
_62
private final String workflowSid;
_62
_62
@Inject
_62
public EnqueueServlet(TwilioAppSettings twilioSettings) {
_62
this.workflowSid = twilioSettings.getWorkflowSid();
_62
}
_62
_62
@Override
_62
public void doPost(HttpServletRequest req, HttpServletResponse resp)
_62
throws ServletException, IOException {
_62
_62
String selectedProduct = getSelectedProduct(req);
_62
Task task = new Task.Builder()
_62
.data(format("{\"selected_product\": \"%s\"}", selectedProduct))
_62
.build();
_62
_62
EnqueueTask enqueueTask = new EnqueueTask.Builder(task).workflowSid(workflowSid).build();
_62
_62
VoiceResponse voiceResponse = new VoiceResponse.Builder().enqueue(enqueueTask).build();
_62
resp.setContentType("application/xml");
_62
try {
_62
resp.getWriter().print(voiceResponse.toXml());
_62
} catch (TwiMLException e) {
_62
LOG.log(Level.SEVERE, e.getMessage(), e);
_62
throw new RuntimeException(e);
_62
}
_62
}
_62
_62
private String getSelectedProduct(HttpServletRequest request) {
_62
return Optional.ofNullable(request.getParameter("Digits"))
_62
.filter(x -> x.equals("1")).map((first) -> "ProgrammableSMS").orElse("ProgrammableVoice");
_62
}
_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


_36
package com.twilio.taskrouter.application.servlet;
_36
_36
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_36
_36
import javax.inject.Inject;
_36
import javax.inject.Singleton;
_36
import javax.json.Json;
_36
import javax.servlet.ServletException;
_36
import javax.servlet.http.HttpServlet;
_36
import javax.servlet.http.HttpServletRequest;
_36
import javax.servlet.http.HttpServletResponse;
_36
import java.io.IOException;
_36
_36
/**
_36
* Servlet for Task assignments
_36
*/
_36
@Singleton
_36
public class AssignmentServlet extends HttpServlet {
_36
_36
private final String dequeueInstruction;
_36
_36
@Inject
_36
public AssignmentServlet(TwilioAppSettings twilioAppSettings) {
_36
dequeueInstruction = Json.createObjectBuilder()
_36
.add("instruction", "dequeue")
_36
.add("post_work_activity_sid", twilioAppSettings.getPostWorkActivitySid())
_36
.build().toString();
_36
}
_36
_36
@Override
_36
public void doPost(HttpServletRequest req, HttpServletResponse resp)
_36
throws ServletException, IOException {
_36
resp.setContentType("application/json");
_36
resp.getWriter().print(dequeueInstruction);
_36
}
_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


_100
package com.twilio.taskrouter.application.servlet;
_100
_100
import com.google.inject.persist.Transactional;
_100
import com.twilio.rest.api.v2010.account.MessageCreator;
_100
import com.twilio.taskrouter.domain.common.TwilioAppSettings;
_100
import com.twilio.taskrouter.domain.model.MissedCall;
_100
import com.twilio.taskrouter.domain.repository.MissedCallRepository;
_100
import com.twilio.type.PhoneNumber;
_100
_100
import javax.inject.Inject;
_100
import javax.inject.Singleton;
_100
import javax.json.Json;
_100
import javax.json.JsonObject;
_100
import javax.servlet.ServletException;
_100
import javax.servlet.http.HttpServlet;
_100
import javax.servlet.http.HttpServletRequest;
_100
import javax.servlet.http.HttpServletResponse;
_100
import java.io.IOException;
_100
import java.io.StringReader;
_100
import java.util.Optional;
_100
import java.util.logging.Logger;
_100
_100
/**
_100
* Servlet for Events callback for missed calls
_100
*/
_100
@Singleton
_100
public class EventsServlet extends HttpServlet {
_100
_100
private static final String LEAVE_MSG = "Sorry, All agents are busy. Please leave a message. "
_100
+ "We will call you as soon as possible";
_100
_100
private static final String OFFLINE_MSG = "Your status has changed to Offline. "
_100
+ "Reply with \"On\" to get back Online";
_100
_100
private static final Logger LOG = Logger.getLogger(EventsServlet.class.getName());
_100
_100
private final TwilioAppSettings twilioSettings;
_100
_100
private final MissedCallRepository missedCallRepository;
_100
_100
@Inject
_100
public EventsServlet(TwilioAppSettings twilioSettings,
_100
MissedCallRepository missedCallRepository) {
_100
this.twilioSettings = twilioSettings;
_100
this.missedCallRepository = missedCallRepository;
_100
}
_100
_100
@Override
_100
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
_100
IOException {
_100
Optional.ofNullable(req.getParameter("EventType"))
_100
.ifPresent(eventName -> {
_100
switch (eventName) {
_100
case "workflow.timeout":
_100
case "task.canceled":
_100
parseAttributes("TaskAttributes", req)
_100
.ifPresent(this::addMissingCallAndLeaveMessage);
_100
break;
_100
case "worker.activity.update":
_100
Optional.ofNullable(req.getParameter("WorkerActivityName"))
_100
.filter("Offline"::equals)
_100
.ifPresent(offlineEvent -> {
_100
parseAttributes("WorkerAttributes", req)
_100
.ifPresent(this::notifyOfflineStatusToWorker);
_100
});
_100
break;
_100
default:
_100
}
_100
});
_100
}
_100
_100
private Optional<JsonObject> parseAttributes(String parameter, HttpServletRequest request) {
_100
return Optional.ofNullable(request.getParameter(parameter))
_100
.map(jsonRequest -> Json.createReader(new StringReader(jsonRequest)).readObject());
_100
}
_100
_100
@Transactional
_100
private void addMissingCallAndLeaveMessage(JsonObject taskAttributesJson) {
_100
String phoneNumber = taskAttributesJson.getString("from");
_100
String selectedProduct = taskAttributesJson.getString("selected_product");
_100
_100
MissedCall missedCall = new MissedCall(phoneNumber, selectedProduct);
_100
missedCallRepository.add(missedCall);
_100
LOG.info("Added Missing Call: " + missedCall);
_100
_100
String callSid = taskAttributesJson.getString("call_sid");
_100
twilioSettings.redirectToVoiceMail(callSid, LEAVE_MSG);
_100
}
_100
_100
private void notifyOfflineStatusToWorker(JsonObject workerAttributesJson) {
_100
String workerPhone = workerAttributesJson.getString("contact_uri");
_100
new MessageCreator(
_100
new PhoneNumber(workerPhone),
_100
new PhoneNumber(twilioSettings.getPhoneNumber().toString()),
_100
OFFLINE_MSG
_100
).create();
_100
_100
}
_100
_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


_63
package com.twilio.taskrouter.application.servlet;
_63
_63
import com.google.inject.Singleton;
_63
import com.twilio.taskrouter.domain.model.WorkspaceFacade;
_63
import com.twilio.twiml.Sms;
_63
import com.twilio.twiml.TwiMLException;
_63
import com.twilio.twiml.VoiceResponse;
_63
_63
import javax.inject.Inject;
_63
import javax.servlet.ServletException;
_63
import javax.servlet.http.HttpServlet;
_63
import javax.servlet.http.HttpServletRequest;
_63
import javax.servlet.http.HttpServletResponse;
_63
import java.io.IOException;
_63
import java.util.Optional;
_63
import java.util.logging.Level;
_63
import java.util.logging.Logger;
_63
_63
/**
_63
* Handles the messages sent by workers for activate/deactivate
_63
* themselves for receiving calls from users
_63
*/
_63
@Singleton
_63
public class MessageServlet extends HttpServlet {
_63
_63
private static final Logger LOG = Logger.getLogger(MessageServlet.class.getName());
_63
_63
private final WorkspaceFacade workspace;
_63
_63
@Inject
_63
public MessageServlet(WorkspaceFacade workspace) {
_63
this.workspace = workspace;
_63
}
_63
_63
@Override
_63
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
_63
throws ServletException, IOException {
_63
final VoiceResponse twimlResponse;
_63
final String newStatus = getNewWorkerStatus(req);
_63
final String workerPhone = req.getParameter("From");
_63
_63
try {
_63
Sms responseSms = workspace.findWorkerByPhone(workerPhone).map(worker -> {
_63
workspace.updateWorkerStatus(worker, newStatus);
_63
return new Sms.Builder(String.format("Your status has changed to %s", newStatus)).build();
_63
}).orElseGet(() -> new Sms.Builder("You are not a valid worker").build());
_63
_63
twimlResponse = new VoiceResponse.Builder().sms(responseSms).build();
_63
resp.setContentType("application/xml");
_63
resp.getWriter().print(twimlResponse.toXml());
_63
_63
} catch (TwiMLException e) {
_63
LOG.log(Level.SEVERE, "Error while providing answer to a workers' sms", e);
_63
}
_63
_63
}
_63
_63
private String getNewWorkerStatus(HttpServletRequest request) {
_63
return Optional.ofNullable(request.getParameter("Body"))
_63
.filter(x -> x.equals("off")).map((first) -> "Offline").orElse("Idle");
_63
}
_63
_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.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think!


Rate this page: