Warm Transfer with Java and Servlets

Download the Code

Warm Transfer

Have you ever been disconnected from a support call while being transferred to someone else?

Warm transfer eliminates this problem. Using Twilio powered warm transfers your agents will have the ability to dial in another agent in real-time.

Here is how it works at a high level:

  1. The first agent becomes available when he/she connects through the web client.
  2. The second agent also becomes available when he/she connects through the web client.
  3. A client calls our support line.
  4. The client stays on hold while the first agent joins the call.
  5. While the first agent is on the phone with the client, he/she can dial a second agent into the call.
  6. Once the second agent is on the call, the first one can disconnect from it. The client and the second agent stay on the call.

Let's get started! Clone the sample application from Github, and click the button below to begin.

Set Up the Voice Webhook

First let's configure the voice webhook for the Twilio number that customers will dial when they want to talk to a support agent. 

Twilio Console for Warm Transfer

This should be the public-facing URL or your app in production: http://***.ngrok.io/conference/connect/client

Loading Code Samples...
Language
package com.twilio.warmtransfer.servlets.guice;

import com.google.inject.Provides;
import com.google.inject.name.Named;
import com.google.inject.servlet.ServletModule;
import com.twilio.http.TwilioRestClient;
import com.twilio.warmtransfer.servlets.CallAgentServlet;
import com.twilio.warmtransfer.servlets.ConferenceWaitServlet;
import com.twilio.warmtransfer.servlets.ConnectAgentServlet;
import com.twilio.warmtransfer.servlets.ConnectClientServlet;
import com.twilio.warmtransfer.servlets.IndexServlet;
import com.twilio.warmtransfer.servlets.TokenServlet;
import com.twilio.warmtransfer.utils.TwilioAuthenticatedActions;

import java.util.Map;

import static java.lang.System.getenv;


public class WarmTransferServletModule extends ServletModule {
    @Override
    public void configureServlets() {
        serve("/").with(IndexServlet.class);
        serve("/token").with(TokenServlet.class);
        serve("/conference/wait").with(ConferenceWaitServlet.class);
        serve("/conference/connect/client").with(ConnectClientServlet.class);
        serve("/conference/connect/agent*").with(ConnectAgentServlet.class);
        serve("/conference/call/agent2").with(CallAgentServlet.class);

        bind(TwilioAuthenticatedActions.class);
    }

    @Provides
    public TwilioRestClient twilioRestClient() {
        return new TwilioRestClient.Builder(
                getenv("TWILIO_ACCOUNT_SID"), getenv("TWILIO_AUTH_TOKEN"))
                .build();
    }

    @Named("env")
    @Provides
    public Map<String, String> env() {
        return System.getenv();
    }
}
src/main/java/com/twilio/warmtransfer/servlets/guice/WarmTransferServletModule.java
Route for our Twilio webhook

src/main/java/com/twilio/warmtransfer/servlets/guice/WarmTransferServletModule.java

Awesome, now you've got a webhook in place.  Next up, we'll look at some of the code.

Connecting an Agent

Here you can see all front-end code necessary to connect an agent using Twilio's Voice Web Client.

We essentially need three things to have a live web client:

  • A capability token (provided by our app)
  • A unique identifier (string) for each agent
  • Event listeners to handle different Twilio-triggered events
Loading Code Samples...
Language
$(function() {
  var currentAgentId;
  var currentConnection;
  var $callStatus = $('#call-status');
  var $connectAgent1Button = $("#connect-agent1-button");
  var $connectAgent2Button = $("#connect-agent2-button");

  var $answerCallButton = $("#answer-call-button");
  var $hangupCallButton = $("#hangup-call-button");
  var $dialAgent2Button = $("#dial-agent2-button");

  $connectAgent1Button.on('click', { agentId: 'agent1' }, agentClickHandler);
  $connectAgent2Button.on('click', { agentId: 'agent2' }, agentClickHandler);
  $hangupCallButton.on('click', hangUp);
  $dialAgent2Button.on('click', dialAgent2);

  function fetchToken(agentId) {
    $.post('token', {"agentId":agentId}, function(data) {
      currentAgentId = data.agentId;
      connectClient(data.token)
    }, 'json');
  }

  function connectClient(token) {
    Twilio.Device.setup(token);
  }

  Twilio.Device.ready(function (device) {
    updateCallStatus("Ready");
    agentConnectedHandler(device._clientName);
  });

  // Callback for when Twilio Client receives a new incoming call
  Twilio.Device.incoming(function(connection) {
    currentConnection = connection;
    updateCallStatus("Incoming support call");

    // Set a callback to be executed when the connection is accepted
    connection.accept(function() {
      updateCallStatus("In call with customer");
      $answerCallButton.prop('disabled', true);
      $hangupCallButton.prop('disabled', false);
      $dialAgent2Button.prop('disabled', false);
    });

    // Set a callback on the answer button and enable it
    $answerCallButton.click(function() {
      connection.accept();
    });
    $answerCallButton.prop('disabled', false);
  });

  /* Report any errors to the call status display */
  Twilio.Device.error(function (error) {
    updateCallStatus("ERROR: " + error.message);
    disableConnectButtons(false);
  });

  // Callback for when the call finalizes
  Twilio.Device.disconnect(function(connection) {
    callEndedHandler(connection.device._clientName);
  });

  function dialAgent2() {
    $.post('/conference/call/agent2')
  }

  /* End a call */
  function hangUp() {
    Twilio.Device.disconnectAll();
  }

  function agentClickHandler(e) {
    var agentId = e.data.agentId;
    disableConnectButtons(true);
    fetchToken(agentId);
  }

  function agentConnectedHandler(agentId) {
    $('#connect-agent-row').addClass('hidden');
    $('#connected-agent-row').removeClass('hidden');
    updateCallStatus("Connected as: " + agentId);

    if (agentId === 'agent1') {
      $dialAgent2Button.removeClass('hidden').prop('disabled', true);
    }
    else {
      $dialAgent2Button.addClass('hidden')
    }
  }

  function callEndedHandler(agentId) {
    $dialAgent2Button.prop('disabled', true);
    $hangupCallButton.prop('disabled', true);
    $answerCallButton.prop('disabled', true)
    updateCallStatus("Connected as: " + agentId);
  }

  function disableConnectButtons(disable) {
    $connectAgent1Button.prop('disabled', disable);
    $connectAgent2Button.prop('disabled', disable);
  }

  function updateCallStatus(status) {
    $callStatus.text(status);
  }
});
src/main/webapp/js/main.js
Create a new call from the browser

src/main/webapp/js/main.js

In the next step we'll take a closer look at capability token generation.

Generate a Capability Token

In order to connect the Twilio Voice Web Client we need a capability token.

To allow incoming connections through the web client an identifier must be provided when generating the token.

Loading Code Samples...
Language
package com.twilio.warmtransfer.utils;


import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.jwt.client.ClientCapability;
import com.twilio.jwt.client.IncomingClientScope;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.type.PhoneNumber;

import java.net.URI;
import java.util.Map;

import static java.util.Arrays.asList;


public class TwilioAuthenticatedActions {
    private Map<String, String> env;
    private String accountSid;
    private String authToken;
    private String twilioNumber;
    private TwilioRestClient twilioRestClient;

    @Inject
    public TwilioAuthenticatedActions(TwilioRestClient twilioRestClient,
            @Named("env") Map<String, String> env) {
        this(env);
        this.twilioRestClient = twilioRestClient;
    }

    TwilioAuthenticatedActions(Map<String, String> env) throws RuntimeException {
        this.env = env;
        if (env.containsKey("TWILIO_ACCOUNT_SID") && env.containsKey("TWILIO_AUTH_TOKEN")
                && env.containsKey("TWILIO_NUMBER")) {
            this.accountSid = env.get("TWILIO_ACCOUNT_SID");
            this.authToken = env.get("TWILIO_AUTH_TOKEN");
            this.twilioNumber = env.get("TWILIO_NUMBER");
        } else {
            throw new RuntimeException("TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_NUMBER must be set on system environment.");
        }
    }

    public String getTokenForAgent(String agentName) {
        ClientCapability clientCapability = new ClientCapability.Builder(accountSid, authToken)
                .scopes(asList(new IncomingClientScope(agentName)))
                .build();

        return clientCapability.toJwt();
    }

    public String callAgent(final String agentId, final String callbackUrl) {
        Call call = Call.creator(new PhoneNumber("client:" + agentId),
                new PhoneNumber(twilioNumber),
                URI.create(callbackUrl)).create(twilioRestClient);
        return call.getSid();
    }
}
src/main/java/com/twilio/warmtransfer/utils/TwilioAuthenticatedActions.java
Generate a Capability Token

src/main/java/com/twilio/warmtransfer/utils/TwilioAuthenticatedActions.java

Next up let's see how to handle incoming calls.

Handle Incoming Calls

For this tutorial we used fixed identifier strings like agent1 and agent2, but you can use any generated string for your call center clients. These identifiers will be used to create outbound calls to the specified agent using the Twilio REST API.

When a client makes a call to our Twilio number the application receives a POST request asking for instructions. We'll use TwiML to instruct the client to join a conference room and the Twilio REST API client to start a call with the first agent, so he can join the same conference.

When providing instructions to the client, we also provide a callback waitUrl, which in our case is another end point of our application. This returns more TwiML to say welcome to the user, and also play some music while on hold. Take a look at the code here

We use the client's CallSid as the conference identifier. Since all participants need this identifier to join the conference, we'll need to store it in a database so that we can grab it when we dial the second agent in later.

Loading Code Samples...
Language
package com.twilio.warmtransfer.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.warmtransfer.services.ActiveCallsService;
import com.twilio.warmtransfer.utils.TwilioAuthenticatedActions;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Singleton
public class ConnectClientServlet extends BaseServlet {

    @Inject
    public ConnectClientServlet(TwilioAuthenticatedActions twilioAuthenticatedActions) {
        super(twilioAuthenticatedActions);
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String conferenceId = request.getParameter("CallSid");

        String callback = makeCallbackURI(request, "/conference/connect/agent1");
        twilioAuthenticatedActions.callAgent("agent1", callback);
        ActiveCallsService.saveNewConference("agent1", conferenceId);

        response.setContentType("text/xml");
        response.getWriter().write(generateConnectConference(conferenceId, false, true));
    }
}
src/main/java/com/twilio/warmtransfer/servlets/ConnectClientServlet.java
Create a conference call, connect the first agent, and put the client on hold

src/main/java/com/twilio/warmtransfer/servlets/ConnectClientServlet.java

Now let's see how to provide TwiML instructions to the client.

Providing TwiML Instruction For The Client

Here we create a VoiceResponse using its Builder, and it will contain a Dial verb with a Conference noun that will instruct the client to join a specific conference room.

Loading Code Samples...
Language
package com.twilio.warmtransfer.utils;

import com.twilio.twiml.Conference;
import com.twilio.twiml.Dial;
import com.twilio.twiml.Method;
import com.twilio.twiml.Play;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

public class TwimlBuilder {
    private VoiceResponse.Builder builder;

    public TwimlBuilder() {
        this.builder = new VoiceResponse.Builder();
    }

    public TwimlBuilder generateWait() throws TwiMLException {
        Say say = new Say.Builder("Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.")
                .build();
        Play play = new Play.Builder("http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3")
                .loop(0)
                .build();
        builder.say(say);
        builder.play(play);
        return this;
    }

    public TwimlBuilder generateConnectConference(String conferenceId, boolean startOnEnter, boolean endOnExit) throws TwiMLException {
        Dial dial = new Dial.Builder()
                .conference(new Conference.Builder(conferenceId)
                        .startConferenceOnEnter(startOnEnter)
                        .endConferenceOnExit(endOnExit)
                        .waitUrl("/conference/wait")
                        .waitMethod(Method.POST)
                        .build())
                .build();
        builder.dial(dial);
        return this;
    }

    public String toEscapedXML() {
        try {
            return this.builder.build().toXml();
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }
}
src/main/java/com/twilio/warmtransfer/utils/TwimlBuilder.java
Generate TwiML for the client to join a conference call

src/main/java/com/twilio/warmtransfer/utils/TwimlBuilder.java

Next up, we will look at how to dial our first agent into the call.

Dialing the First Agent Into the Call

For our app we created a callAgent method to handle dialing our agents. This method uses Twilio's Sdk Call class to create a new call. The create static method receives the following parameters:

  1. To : The agent web client's identifier (agent1 or agent2)
  2. From: Your Twilio number
  3. Url : A URL to ask for TwiML instructions when the call connects

The create method returns an object which contains the execute method and the call to Twilio's API only happens when this execute method is called.

Once the agent answers the call in the web client, a request is made to the callback URL instructing this call to join the conference room where the client is already waiting.

Loading Code Samples...
Language
package com.twilio.warmtransfer.utils;


import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.jwt.client.ClientCapability;
import com.twilio.jwt.client.IncomingClientScope;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.type.PhoneNumber;

import java.net.URI;
import java.util.Map;

import static java.util.Arrays.asList;


public class TwilioAuthenticatedActions {
    private Map<String, String> env;
    private String accountSid;
    private String authToken;
    private String twilioNumber;
    private TwilioRestClient twilioRestClient;

    @Inject
    public TwilioAuthenticatedActions(TwilioRestClient twilioRestClient,
            @Named("env") Map<String, String> env) {
        this(env);
        this.twilioRestClient = twilioRestClient;
    }

    TwilioAuthenticatedActions(Map<String, String> env) throws RuntimeException {
        this.env = env;
        if (env.containsKey("TWILIO_ACCOUNT_SID") && env.containsKey("TWILIO_AUTH_TOKEN")
                && env.containsKey("TWILIO_NUMBER")) {
            this.accountSid = env.get("TWILIO_ACCOUNT_SID");
            this.authToken = env.get("TWILIO_AUTH_TOKEN");
            this.twilioNumber = env.get("TWILIO_NUMBER");
        } else {
            throw new RuntimeException("TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_NUMBER must be set on system environment.");
        }
    }

    public String getTokenForAgent(String agentName) {
        ClientCapability clientCapability = new ClientCapability.Builder(accountSid, authToken)
                .scopes(asList(new IncomingClientScope(agentName)))
                .build();

        return clientCapability.toJwt();
    }

    public String callAgent(final String agentId, final String callbackUrl) {
        Call call = Call.creator(new PhoneNumber("client:" + agentId),
                new PhoneNumber(twilioNumber),
                URI.create(callbackUrl)).create(twilioRestClient);
        return call.getSid();
    }
}
src/main/java/com/twilio/warmtransfer/utils/TwilioAuthenticatedActions.java
Dial an agent into the call

src/main/java/com/twilio/warmtransfer/utils/TwilioAuthenticatedActions.java

With that in mind, let's see how to add the second agent to the call.

Dialing the Second Agent Into the Call

When the client and the first agent are both in the call we are ready to perform a warm transfer to a second agent. The first agent makes a request and we look for his conferenceId needed to dial the second agent in. Since we already have a callAgent method we can simply call it to connect the second agent in.

Loading Code Samples...
Language
package com.twilio.warmtransfer.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.warmtransfer.services.ActiveCallsService;
import com.twilio.warmtransfer.utils.TwilioAuthenticatedActions;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Singleton
public class CallAgentServlet extends BaseServlet {

    @Inject
    public CallAgentServlet(TwilioAuthenticatedActions twilioAuthenticatedActions) {
        super(twilioAuthenticatedActions);
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String agentId = "agent2";
        String conferenceId = ActiveCallsService.getConferenceFromAgentID("agent1");
        String callbackUrl = makeCallbackURI(request, "/conference/connect/" + agentId);
        twilioAuthenticatedActions.callAgent(agentId, callbackUrl);
        ActiveCallsService.saveNewConference(agentId, conferenceId);
    }
}
src/main/java/com/twilio/warmtransfer/servlets/CallAgentServlet.java
Invite a second agent to the conference

src/main/java/com/twilio/warmtransfer/servlets/CallAgentServlet.java

Next up, we'll look at how to handle the first agent leaving the call.

The First Agent Leaves the Call

When the three participants have joined the same call, the first agent has served his purpose. Now he can drop the call, leaving agent two and the client to have a pleasant conversation.

It is important to notice the differences between the TwiML each one of the participants received when joining the call. Both, agent one and two, have startConferenceOnEnter set to true. This means the conference will start when any of them joins the call. For the client calling and for agent two endConferenceOnExit is set to true. This causes the call to end when either of these two participants drops the call.

Loading Code Samples...
Language
package com.twilio.warmtransfer.utils;

import com.twilio.twiml.Conference;
import com.twilio.twiml.Dial;
import com.twilio.twiml.Method;
import com.twilio.twiml.Play;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

public class TwimlBuilder {
    private VoiceResponse.Builder builder;

    public TwimlBuilder() {
        this.builder = new VoiceResponse.Builder();
    }

    public TwimlBuilder generateWait() throws TwiMLException {
        Say say = new Say.Builder("Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.")
                .build();
        Play play = new Play.Builder("http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3")
                .loop(0)
                .build();
        builder.say(say);
        builder.play(play);
        return this;
    }

    public TwimlBuilder generateConnectConference(String conferenceId, boolean startOnEnter, boolean endOnExit) throws TwiMLException {
        Dial dial = new Dial.Builder()
                .conference(new Conference.Builder(conferenceId)
                        .startConferenceOnEnter(startOnEnter)
                        .endConferenceOnExit(endOnExit)
                        .waitUrl("/conference/wait")
                        .waitMethod(Method.POST)
                        .build())
                .build();
        builder.dial(dial);
        return this;
    }

    public String toEscapedXML() {
        try {
            return this.builder.build().toXml();
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }
}
src/main/java/com/twilio/warmtransfer/utils/TwimlBuilder.java
Instruction to dial into a conference call

src/main/java/com/twilio/warmtransfer/utils/TwimlBuilder.java

That's it! We have just implemented warm transfers using Java Servlets. Now your clients won't get disconnect from support calls while they are being transferred to some else.

Where to next?

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

Click-To-Call

Click-to-call enables your company to convert web traffic into phone calls with the click of a button. Learn how to implement it in minutes.

Automated-Surveys

Learn to implement automated surveys in your Spring web app with Twilio.

Did this help?

Thanks for checking this tutorial out! Let us know on Twitter what you've built... or what you're building.

Agustin Camino
David Prothero
Samuel Mendes

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
package com.twilio.warmtransfer.servlets.guice;

import com.google.inject.Provides;
import com.google.inject.name.Named;
import com.google.inject.servlet.ServletModule;
import com.twilio.http.TwilioRestClient;
import com.twilio.warmtransfer.servlets.CallAgentServlet;
import com.twilio.warmtransfer.servlets.ConferenceWaitServlet;
import com.twilio.warmtransfer.servlets.ConnectAgentServlet;
import com.twilio.warmtransfer.servlets.ConnectClientServlet;
import com.twilio.warmtransfer.servlets.IndexServlet;
import com.twilio.warmtransfer.servlets.TokenServlet;
import com.twilio.warmtransfer.utils.TwilioAuthenticatedActions;

import java.util.Map;

import static java.lang.System.getenv;


public class WarmTransferServletModule extends ServletModule {
    @Override
    public void configureServlets() {
        serve("/").with(IndexServlet.class);
        serve("/token").with(TokenServlet.class);
        serve("/conference/wait").with(ConferenceWaitServlet.class);
        serve("/conference/connect/client").with(ConnectClientServlet.class);
        serve("/conference/connect/agent*").with(ConnectAgentServlet.class);
        serve("/conference/call/agent2").with(CallAgentServlet.class);

        bind(TwilioAuthenticatedActions.class);
    }

    @Provides
    public TwilioRestClient twilioRestClient() {
        return new TwilioRestClient.Builder(
                getenv("TWILIO_ACCOUNT_SID"), getenv("TWILIO_AUTH_TOKEN"))
                .build();
    }

    @Named("env")
    @Provides
    public Map<String, String> env() {
        return System.getenv();
    }
}
$(function() {
  var currentAgentId;
  var currentConnection;
  var $callStatus = $('#call-status');
  var $connectAgent1Button = $("#connect-agent1-button");
  var $connectAgent2Button = $("#connect-agent2-button");

  var $answerCallButton = $("#answer-call-button");
  var $hangupCallButton = $("#hangup-call-button");
  var $dialAgent2Button = $("#dial-agent2-button");

  $connectAgent1Button.on('click', { agentId: 'agent1' }, agentClickHandler);
  $connectAgent2Button.on('click', { agentId: 'agent2' }, agentClickHandler);
  $hangupCallButton.on('click', hangUp);
  $dialAgent2Button.on('click', dialAgent2);

  function fetchToken(agentId) {
    $.post('token', {"agentId":agentId}, function(data) {
      currentAgentId = data.agentId;
      connectClient(data.token)
    }, 'json');
  }

  function connectClient(token) {
    Twilio.Device.setup(token);
  }

  Twilio.Device.ready(function (device) {
    updateCallStatus("Ready");
    agentConnectedHandler(device._clientName);
  });

  // Callback for when Twilio Client receives a new incoming call
  Twilio.Device.incoming(function(connection) {
    currentConnection = connection;
    updateCallStatus("Incoming support call");

    // Set a callback to be executed when the connection is accepted
    connection.accept(function() {
      updateCallStatus("In call with customer");
      $answerCallButton.prop('disabled', true);
      $hangupCallButton.prop('disabled', false);
      $dialAgent2Button.prop('disabled', false);
    });

    // Set a callback on the answer button and enable it
    $answerCallButton.click(function() {
      connection.accept();
    });
    $answerCallButton.prop('disabled', false);
  });

  /* Report any errors to the call status display */
  Twilio.Device.error(function (error) {
    updateCallStatus("ERROR: " + error.message);
    disableConnectButtons(false);
  });

  // Callback for when the call finalizes
  Twilio.Device.disconnect(function(connection) {
    callEndedHandler(connection.device._clientName);
  });

  function dialAgent2() {
    $.post('/conference/call/agent2')
  }

  /* End a call */
  function hangUp() {
    Twilio.Device.disconnectAll();
  }

  function agentClickHandler(e) {
    var agentId = e.data.agentId;
    disableConnectButtons(true);
    fetchToken(agentId);
  }

  function agentConnectedHandler(agentId) {
    $('#connect-agent-row').addClass('hidden');
    $('#connected-agent-row').removeClass('hidden');
    updateCallStatus("Connected as: " + agentId);

    if (agentId === 'agent1') {
      $dialAgent2Button.removeClass('hidden').prop('disabled', true);
    }
    else {
      $dialAgent2Button.addClass('hidden')
    }
  }

  function callEndedHandler(agentId) {
    $dialAgent2Button.prop('disabled', true);
    $hangupCallButton.prop('disabled', true);
    $answerCallButton.prop('disabled', true)
    updateCallStatus("Connected as: " + agentId);
  }

  function disableConnectButtons(disable) {
    $connectAgent1Button.prop('disabled', disable);
    $connectAgent2Button.prop('disabled', disable);
  }

  function updateCallStatus(status) {
    $callStatus.text(status);
  }
});
package com.twilio.warmtransfer.utils;


import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.jwt.client.ClientCapability;
import com.twilio.jwt.client.IncomingClientScope;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.type.PhoneNumber;

import java.net.URI;
import java.util.Map;

import static java.util.Arrays.asList;


public class TwilioAuthenticatedActions {
    private Map<String, String> env;
    private String accountSid;
    private String authToken;
    private String twilioNumber;
    private TwilioRestClient twilioRestClient;

    @Inject
    public TwilioAuthenticatedActions(TwilioRestClient twilioRestClient,
            @Named("env") Map<String, String> env) {
        this(env);
        this.twilioRestClient = twilioRestClient;
    }

    TwilioAuthenticatedActions(Map<String, String> env) throws RuntimeException {
        this.env = env;
        if (env.containsKey("TWILIO_ACCOUNT_SID") && env.containsKey("TWILIO_AUTH_TOKEN")
                && env.containsKey("TWILIO_NUMBER")) {
            this.accountSid = env.get("TWILIO_ACCOUNT_SID");
            this.authToken = env.get("TWILIO_AUTH_TOKEN");
            this.twilioNumber = env.get("TWILIO_NUMBER");
        } else {
            throw new RuntimeException("TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_NUMBER must be set on system environment.");
        }
    }

    public String getTokenForAgent(String agentName) {
        ClientCapability clientCapability = new ClientCapability.Builder(accountSid, authToken)
                .scopes(asList(new IncomingClientScope(agentName)))
                .build();

        return clientCapability.toJwt();
    }

    public String callAgent(final String agentId, final String callbackUrl) {
        Call call = Call.creator(new PhoneNumber("client:" + agentId),
                new PhoneNumber(twilioNumber),
                URI.create(callbackUrl)).create(twilioRestClient);
        return call.getSid();
    }
}
package com.twilio.warmtransfer.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.warmtransfer.services.ActiveCallsService;
import com.twilio.warmtransfer.utils.TwilioAuthenticatedActions;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Singleton
public class ConnectClientServlet extends BaseServlet {

    @Inject
    public ConnectClientServlet(TwilioAuthenticatedActions twilioAuthenticatedActions) {
        super(twilioAuthenticatedActions);
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String conferenceId = request.getParameter("CallSid");

        String callback = makeCallbackURI(request, "/conference/connect/agent1");
        twilioAuthenticatedActions.callAgent("agent1", callback);
        ActiveCallsService.saveNewConference("agent1", conferenceId);

        response.setContentType("text/xml");
        response.getWriter().write(generateConnectConference(conferenceId, false, true));
    }
}
package com.twilio.warmtransfer.utils;

import com.twilio.twiml.Conference;
import com.twilio.twiml.Dial;
import com.twilio.twiml.Method;
import com.twilio.twiml.Play;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

public class TwimlBuilder {
    private VoiceResponse.Builder builder;

    public TwimlBuilder() {
        this.builder = new VoiceResponse.Builder();
    }

    public TwimlBuilder generateWait() throws TwiMLException {
        Say say = new Say.Builder("Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.")
                .build();
        Play play = new Play.Builder("http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3")
                .loop(0)
                .build();
        builder.say(say);
        builder.play(play);
        return this;
    }

    public TwimlBuilder generateConnectConference(String conferenceId, boolean startOnEnter, boolean endOnExit) throws TwiMLException {
        Dial dial = new Dial.Builder()
                .conference(new Conference.Builder(conferenceId)
                        .startConferenceOnEnter(startOnEnter)
                        .endConferenceOnExit(endOnExit)
                        .waitUrl("/conference/wait")
                        .waitMethod(Method.POST)
                        .build())
                .build();
        builder.dial(dial);
        return this;
    }

    public String toEscapedXML() {
        try {
            return this.builder.build().toXml();
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }
}
package com.twilio.warmtransfer.utils;


import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.jwt.client.ClientCapability;
import com.twilio.jwt.client.IncomingClientScope;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.type.PhoneNumber;

import java.net.URI;
import java.util.Map;

import static java.util.Arrays.asList;


public class TwilioAuthenticatedActions {
    private Map<String, String> env;
    private String accountSid;
    private String authToken;
    private String twilioNumber;
    private TwilioRestClient twilioRestClient;

    @Inject
    public TwilioAuthenticatedActions(TwilioRestClient twilioRestClient,
            @Named("env") Map<String, String> env) {
        this(env);
        this.twilioRestClient = twilioRestClient;
    }

    TwilioAuthenticatedActions(Map<String, String> env) throws RuntimeException {
        this.env = env;
        if (env.containsKey("TWILIO_ACCOUNT_SID") && env.containsKey("TWILIO_AUTH_TOKEN")
                && env.containsKey("TWILIO_NUMBER")) {
            this.accountSid = env.get("TWILIO_ACCOUNT_SID");
            this.authToken = env.get("TWILIO_AUTH_TOKEN");
            this.twilioNumber = env.get("TWILIO_NUMBER");
        } else {
            throw new RuntimeException("TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_NUMBER must be set on system environment.");
        }
    }

    public String getTokenForAgent(String agentName) {
        ClientCapability clientCapability = new ClientCapability.Builder(accountSid, authToken)
                .scopes(asList(new IncomingClientScope(agentName)))
                .build();

        return clientCapability.toJwt();
    }

    public String callAgent(final String agentId, final String callbackUrl) {
        Call call = Call.creator(new PhoneNumber("client:" + agentId),
                new PhoneNumber(twilioNumber),
                URI.create(callbackUrl)).create(twilioRestClient);
        return call.getSid();
    }
}
package com.twilio.warmtransfer.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.warmtransfer.services.ActiveCallsService;
import com.twilio.warmtransfer.utils.TwilioAuthenticatedActions;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Singleton
public class CallAgentServlet extends BaseServlet {

    @Inject
    public CallAgentServlet(TwilioAuthenticatedActions twilioAuthenticatedActions) {
        super(twilioAuthenticatedActions);
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String agentId = "agent2";
        String conferenceId = ActiveCallsService.getConferenceFromAgentID("agent1");
        String callbackUrl = makeCallbackURI(request, "/conference/connect/" + agentId);
        twilioAuthenticatedActions.callAgent(agentId, callbackUrl);
        ActiveCallsService.saveNewConference(agentId, conferenceId);
    }
}
package com.twilio.warmtransfer.utils;

import com.twilio.twiml.Conference;
import com.twilio.twiml.Dial;
import com.twilio.twiml.Method;
import com.twilio.twiml.Play;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

public class TwimlBuilder {
    private VoiceResponse.Builder builder;

    public TwimlBuilder() {
        this.builder = new VoiceResponse.Builder();
    }

    public TwimlBuilder generateWait() throws TwiMLException {
        Say say = new Say.Builder("Thank you for calling. Please wait in line for a few seconds. An agent will be with you shortly.")
                .build();
        Play play = new Play.Builder("http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3")
                .loop(0)
                .build();
        builder.say(say);
        builder.play(play);
        return this;
    }

    public TwimlBuilder generateConnectConference(String conferenceId, boolean startOnEnter, boolean endOnExit) throws TwiMLException {
        Dial dial = new Dial.Builder()
                .conference(new Conference.Builder(conferenceId)
                        .startConferenceOnEnter(startOnEnter)
                        .endConferenceOnExit(endOnExit)
                        .waitUrl("/conference/wait")
                        .waitMethod(Method.POST)
                        .build())
                .build();
        builder.dial(dial);
        return this;
    }

    public String toEscapedXML() {
        try {
            return this.builder.build().toXml();
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }
}