Automated Survey with Java and Spring

Have you ever wondered how to create an automated survey that can be answered over phone or SMS?

This tutorial will show how to do it using the Twilio API.

Here's how it works at a high level

Automated Survey Diagram

  1. The end user calls or sends an SMS to the survey phone number.
  2. Twilio gets the call or text and makes an HTTP request to your application asking for instructions on how to respond.
  3. Your web application instructs Twilio (using TwiML) to Gather or Record the user input over the phone, and prompt for text input with Message if you are using SMS.
  4. After each question, Twilio makes another request to your server with the user's input, which your application stores in its database.
  5. After storing the answer, our server will instruct Twilio to Redirect the user to the next question or finish the survey.

Instacart uses Twilio to power their customer service surveys and integrate that feedback into their customer database. Read more here.

Loading Code Samples...
Language
package com.twilio.survey;

import com.twilio.survey.repositories.QuestionRepository;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.QuestionService;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.SurveyParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SurveyJavaApplication implements CommandLineRunner {
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private SurveyRepository surveyRepository;

    public static void main(String[] args) {
        SpringApplication.run(SurveyJavaApplication.class, args);
    }

    /**
     * Method that runs on app initialization. It will parse and insert the questions in the DB
     * on every app initialization
     */
    @Override
    public void run(String... strings) throws Exception {
        SurveyService surveyService = new SurveyService(surveyRepository);
        QuestionService questionService = new QuestionService(questionRepository);

        SurveyParser surveyParser = new SurveyParser(surveyService, questionService);
        surveyParser.parse("survey.json");
    }
}
src/main/java/com/twilio/survey/SurveyJavaApplication.java
Initialize the Java application

src/main/java/com/twilio/survey/SurveyJavaApplication.java

Creating a Survey

In order to perform automated surveys we first need to have some questions to ask. For your convenience, this application's repository already includes one survey that can be loaded into the database. If the database is configured correctly this survey will be loaded each time the app starts.

You can modify the questions from the survey by editing the survey.json file located in the root of the repository and re-running the app.

Loading Code Samples...
Language
package com.twilio.survey.util;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.survey.services.QuestionService;
import com.twilio.survey.services.SurveyService;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Date;

/**
 * Class in charge of parsing the JSON file that contains the list of questions.
 */
public class SurveyParser {
    SurveyService surveyService;
    QuestionService questionService;

    public SurveyParser() {
    }

    public SurveyParser(SurveyService surveyService, QuestionService questionService) {
        this.surveyService = surveyService;
        this.questionService = questionService;
    }

    /**
     * This method is called on app initialization.
     * It will insert the questions in the DB every time the application starts
     *
     * @param filePath path for the .json file
     */
    public void parse(String filePath) {
        FileReader reader = null;
        try {
            reader = new FileReader(filePath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        JSONParser jsonParser = new JSONParser();
        JSONObject jsonObject = null;
        try {
            jsonObject = (JSONObject) jsonParser.parse(reader);
        } catch (IOException e) {
            System.out.println("Error reading from survey file");
        } catch (ParseException e) {
            System.out.println("Error while parsing JSON survey file");
        }

        String title = (String) jsonObject.get("title");
        JSONArray questions = (JSONArray) jsonObject.get("questions");

        insertSurveyIntoDb(title, questions);
    }

    @Transactional
    private void insertSurveyIntoDb(String title, JSONArray questions) {
        Survey survey = new Survey(title, new Date());
        surveyService.create(survey);
        Question newQuestion;

        for (Object question : questions) {
            JSONObject obj = (JSONObject) question;
            String body = (String) obj.get("body");
            String type = (String) obj.get("type");
            newQuestion = new Question(body, type, survey, new Date());
            questionService.save(newQuestion);
        }
    }
}
src/main/java/com/twilio/survey/util/SurveyParser.java
Seed database with survey questions

src/main/java/com/twilio/survey/util/SurveyParser.java

We want our users to have a way to take this survey, so we still need to implement a handler for SMS and calls. First, let's take a moment to understand the flow of a Twilio-powered survey as an interview loop.

The Interview Loop

It is helpful to visualize your interaction with a user during a survey as a loop. The chart below shows how that interaction is handled:

Survey Flow Chart

The user can answer a question for your survey over the phone using either their phone's keypad or by speaking. After each interaction, Twilio makes an HTTP request to your web application with either the string of keys the user pressed or a URL to a recording of their voice input.

For SMS surveys the user will answer questions by replying with another SMS to the Twilio number that sent the question.

It's up to the application to process, store and respond to the user's input.

Let's dive into this flow to see how it actually works.

Configuring a Twilio Number

To initiate the interview process, we need to configure one of our Twilio numbers to send our web application an HTTP request when we get an incoming call or text.

Survey Webhook Config

Click on one of your numbers and configure Voice and Message URLs that point to your server. In our code, the routes are /survey/call and /survey/sms, respectively.

If you don't already have a server configured to use as your webhook, ngrok is a great tool for testing webhooks locally.

Loading Code Samples...
Language
package com.twilio.survey.controllers;

import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.TwiMLUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
public class SurveyController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;

    public SurveyController() {
    }

    /**
     * Calls endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * Otherwise it plays a message and hang up the call if there is no survey available.
     */
    @RequestMapping(value = "/survey/call", method = RequestMethod.GET)
    public void call(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();

        if (lastSurvey != null) {
            response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
        } else {
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    /**
     * SMS endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * As SMS is just a message instead of a long running call, we store state by mapping a Twilio's Cookie to a Session
     */
    @RequestMapping(value = "/survey/sms", method = RequestMethod.GET)
    public void sms(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();
        HttpSession session = request.getSession(false);

        if (lastSurvey != null) {
            if (session == null || session.isNew()) {
                // New session,
                response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
            } else {
                // Ongoing session, redirect to ResponseController to save it's answer.
                response.getWriter().print(getSaveResponseRedirect(session));
            }
        } else {
            // No survey
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    private String getSaveResponseRedirect(HttpSession session) throws Exception {
        String saveURL = "/save_response?qid=" + getQuestionIdFromSession(session);
        return TwiMLUtil.redirectPost(saveURL);
    }

    /**
     * Creates the TwiMLResponse for the first question of the survey
     *
     * @param survey  Survey entity
     * @param request HttpServletRequest request
     * @return TwiMLResponse
     */
    private String getFirstQuestionRedirect(Survey survey, HttpServletRequest request) throws Exception {
        String welcomeMessage = "Welcome to the " + survey.getTitle() + " survey";
        String questionURL = "/question?survey=" + survey.getId() + "&question=1";
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponseWithRedirect(welcomeMessage, questionURL);
        } else {
            return TwiMLUtil.voiceResponseWithRedirect(welcomeMessage, questionURL);
        }
    }

    /**
     * Creates a TwiMLResponse if no surveys are found on the database
     * For SMS, it's just a message
     * For Voice it should also send a Hangup to the ongoing call
     *
     * @return TwiMLResponse
     */
    private String getHangupResponse(HttpServletRequest request) throws Exception {
        String errorMessage = "We are sorry, there are no surveys available. Good bye.";
        cleanSession(request);
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponse(errorMessage);
        } else {
            return TwiMLUtil.voiceResponse(errorMessage);
        }
    }

    private void cleanSession(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
    }

    private Long getQuestionIdFromSession(HttpSession session) {
        return (Long) session.getAttribute("questionId");
    }
}
src/main/java/com/twilio/survey/controllers/SurveyController.java
Endpoints for voice and sms requests to your survey

src/main/java/com/twilio/survey/controllers/SurveyController.java

You've configured your webhooks in the Twilio Console. Let's learn how to handle requests to our Twilio endpoints.

Responding to a Twilio Request

Right after receiving a call or an SMS, Twilio sends a request to the URL specified in our phone number configuration (/survey/call for calls and /survey/sms for sms).

Each of these endpoints will receive the request and will use a TwiMLUtil to return a welcome message to the user. For voice call users, the message will contain a Say verb with the message, whereas if the user is interacting with our survey over SMS, the message will use a Message verb.

We will also include a Redirect verb pointing to the question's endpoint in order to continue the survey flow.

Loading Code Samples...
Language
package com.twilio.survey.controllers;

import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.TwiMLUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
public class SurveyController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;

    public SurveyController() {
    }

    /**
     * Calls endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * Otherwise it plays a message and hang up the call if there is no survey available.
     */
    @RequestMapping(value = "/survey/call", method = RequestMethod.GET)
    public void call(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();

        if (lastSurvey != null) {
            response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
        } else {
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    /**
     * SMS endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * As SMS is just a message instead of a long running call, we store state by mapping a Twilio's Cookie to a Session
     */
    @RequestMapping(value = "/survey/sms", method = RequestMethod.GET)
    public void sms(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();
        HttpSession session = request.getSession(false);

        if (lastSurvey != null) {
            if (session == null || session.isNew()) {
                // New session,
                response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
            } else {
                // Ongoing session, redirect to ResponseController to save it's answer.
                response.getWriter().print(getSaveResponseRedirect(session));
            }
        } else {
            // No survey
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    private String getSaveResponseRedirect(HttpSession session) throws Exception {
        String saveURL = "/save_response?qid=" + getQuestionIdFromSession(session);
        return TwiMLUtil.redirectPost(saveURL);
    }

    /**
     * Creates the TwiMLResponse for the first question of the survey
     *
     * @param survey  Survey entity
     * @param request HttpServletRequest request
     * @return TwiMLResponse
     */
    private String getFirstQuestionRedirect(Survey survey, HttpServletRequest request) throws Exception {
        String welcomeMessage = "Welcome to the " + survey.getTitle() + " survey";
        String questionURL = "/question?survey=" + survey.getId() + "&question=1";
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponseWithRedirect(welcomeMessage, questionURL);
        } else {
            return TwiMLUtil.voiceResponseWithRedirect(welcomeMessage, questionURL);
        }
    }

    /**
     * Creates a TwiMLResponse if no surveys are found on the database
     * For SMS, it's just a message
     * For Voice it should also send a Hangup to the ongoing call
     *
     * @return TwiMLResponse
     */
    private String getHangupResponse(HttpServletRequest request) throws Exception {
        String errorMessage = "We are sorry, there are no surveys available. Good bye.";
        cleanSession(request);
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponse(errorMessage);
        } else {
            return TwiMLUtil.voiceResponse(errorMessage);
        }
    }

    private void cleanSession(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
    }

    private Long getQuestionIdFromSession(HttpSession session) {
        return (Long) session.getAttribute("questionId");
    }
}
src/main/java/com/twilio/survey/controllers/SurveyController.java
Welcome a user and redirect to the question controller

src/main/java/com/twilio/survey/controllers/SurveyController.java

We've seen how to handle requests to our webhooks. Now let's respond to some messages.

Question Controller

This endpoint will check to see if our inbound request is an SMS or voice call, instantiating the proper class to build the correct TwiML response. Each type of question and interaction (Call or SMS) will produce different instructions on how to proceed. For instance, we can record voice or gather a key press during a call, but we can't do the same for text messages.

When the user is interacting with our survey over SMS we don't have something like an ongoing call session with a well defined state. It becomes harder to know if an SMS is answering question 2 or 20, since all requests will be sent to our /survey/sms main endpoint. To solve that, we can use Twilio Cookies to keep track of what question is being answered at a given moment. This is done with the createSessionForQuestion method.

Loading Code Samples...
Language
package com.twilio.survey.controllers;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.QuestionBuilder;
import com.twilio.survey.util.SMSQuestionBuilder;
import com.twilio.survey.util.VoiceQuestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
public class QuestionController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;

    public QuestionController() {
    }

    /**
     * End point that returns the appropriate question response based on the parameters it receives
     */
    @RequestMapping(value = "/question", method = RequestMethod.GET, produces="application/xml")
    public void show(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);
        Survey survey = surveyService.find(Long.parseLong(request.getParameter("survey")));

        Question currentQuestion = survey.getQuestionByNumber(Integer.parseInt(request.getParameter("question")));
        QuestionBuilder builder = getQuestionHandler(currentQuestion, request);

        if (currentQuestion != null) {
            response.getWriter().print(builder.build());
        } else {
            response.getWriter().print(builder.buildNoMoreQuestions());
        }
        response.setContentType("application/xml");
    }

    private void createSessionForQuestion(HttpServletRequest request, Question currentQuestion) {
        if (currentQuestion == null) {
            return;
        }
        HttpSession session = request.getSession(true);
        session.setAttribute("questionId", currentQuestion.getId());
    }

    private QuestionBuilder getQuestionHandler(Question currentQuestion, HttpServletRequest request) {
        if (isVoiceRequest(request)) {
            return new VoiceQuestionBuilder(currentQuestion);
        } else {
            createSessionForQuestion(request, currentQuestion);
            return new SMSQuestionBuilder(currentQuestion);
        }
    }

    private boolean isVoiceRequest(HttpServletRequest request) {
        return request.getParameter("MessageSid") == null;
    }
}
src/main/java/com/twilio/survey/controllers/QuestionController.java
Create and manage survey sessions

src/main/java/com/twilio/survey/controllers/QuestionController.java

Next, we'll see how to build TwiML to handle responses to our survey questions.

Building Our TwiML Verbs

If the question is "numeric" or "yes-no" in nature, we need to use the <Gather> verb. However, if we expect the user to record a free-form voice answer we need to use the <Record> verb. Both verbs take an action attribute and a method attribute.

Twilio will use both attributes to define our response's endpoint as a callback. This endpoint is responsible for receiving and storing the caller's answer.

During the Record verb creation, we also ask Twilio for a Transcription. Twilio will process the voice recording and extract all useful text, making a request to our response endpoint when the transcription is complete.

Loading Code Samples...
Language
package com.twilio.survey.util;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.twiml.Body;
import com.twilio.twiml.Gather;
import com.twilio.twiml.Hangup;
import com.twilio.twiml.Message;
import com.twilio.twiml.MessagingResponse;
import com.twilio.twiml.Method;
import com.twilio.twiml.Record;
import com.twilio.twiml.Redirect;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiML;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

public class TwiMLUtil {

    public static String redirect(int nextQuestionNumber, Survey survey) throws TwiMLException {
        String nextQuestionURL = "/question?survey=" + survey.getId() + "&question=" + nextQuestionNumber;
        return redirect(nextQuestionURL, Method.GET).toXml();
    }

    public static String redirectPost(String url) throws TwiMLException {
        return redirect(url, Method.POST).toXml();
    }

    private static TwiML redirect(String url, Method method) {
        return new VoiceResponse.Builder()
                .redirect(new Redirect.Builder().url(url).method(method).build())
                .build();
    }

    public static Record record(Question question) {
        return new Record.Builder()
                .action("/save_response?qid=" + question.getId())
                .method(Method.POST)
                .transcribe(true)
                .transcribeCallback("/save_response?qid=" + question.getId())
                .maxLength(60)
                .build();
    }

    public static Gather gather(Question question) {
        return new Gather.Builder()
                .action("/save_response?qid=" + question.getId())
                .method(Method.POST)
                .finishOnKey("#")
                .build();
    }

    public static String voiceResponse(String message) throws TwiMLException {
        return new VoiceResponse.Builder()
                .say(new Say.Builder(message).build())
                .hangup(new Hangup())
                .build()
                .toXml();
    }

    public static String messagingResponse(String message) throws TwiMLException {
        return new MessagingResponse.Builder()
                .message(new Message.Builder().body(new Body(message)).build())
                .build()
                .toXml();
    }

    public static String voiceResponseWithRedirect(String message, String questionUrl) throws TwiMLException {
        return new VoiceResponse.Builder()
                .say(new Say.Builder(message).build())
                .redirect(new Redirect.Builder().url(questionUrl).method(Method.GET).build())
                .build()
                .toXml();
    }

    public static String messagingResponseWithRedirect(String message, String redirectUrl) throws TwiMLException {
        return new MessagingResponse.Builder()
                .message(new Message.Builder().body(new Body(message)).build())
                .redirect(new Redirect.Builder().url(redirectUrl).method(Method.GET).build())
                .build()
                .toXml();
    }
}
src/main/java/com/twilio/survey/util/TwiMLUtil.java
Record and gather survey responses

src/main/java/com/twilio/survey/util/TwiMLUtil.java

We've seen how to generate questions with TwiML. Now, lets see how to handle the responses.

Handling Responses

After the user has finished speaking and pressing keys, Twilio sends a request telling us what happened and asking for further instructions.

At this point, we need to recover data from Twilio's request parameters (ResponseParser does this) and store them with our persistResponse method.

Recovered parameters vary according to what we asked in our survey questions:

  • Body contains the text message from an answer sent over SMS.
  • Digits contains the keys pressed for a numeric question.
  • RecodingUrl contains the URL for listening to a recorded message.
  • TranscriptionText contains the text of a voice recording.

Finally we redirect to our Question controller, which will ask the next question in the loop. This is done in the redirectToNextQuestion method.

Loading Code Samples...
Language
package com.twilio.survey.controllers;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Response;
import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.QuestionRepository;
import com.twilio.survey.repositories.ResponseRepository;
import com.twilio.survey.services.QuestionService;
import com.twilio.survey.services.ResponseService;
import com.twilio.survey.util.ResponseParser;
import com.twilio.survey.util.TwiMLUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Controller
public class ResponseController {
    @Autowired
    private QuestionRepository questionRepository;
    private QuestionService questionService;
    @Autowired
    private ResponseRepository responseRepository;
    private ResponseService responseService;

    public ResponseController() {
    }

    /**
     * End point that saves a question response and redirects the call to the next question,
     * if one is available.
     */
    @RequestMapping(value = "/save_response", method = RequestMethod.POST, produces="application/xml")
    public void save(HttpServletRequest request, HttpServletResponse response) throws Exception {
        PrintWriter responseWriter = response.getWriter();
        this.questionService = new QuestionService(questionRepository);
        this.responseService = new ResponseService(responseRepository);

        Question currentQuestion = getQuestionFromRequest(request);
        Survey survey = currentQuestion.getSurvey();
        persistResponse(new ResponseParser(currentQuestion, request).parse());

        if (survey.isLastQuestion(currentQuestion)) {
            String message = "Tank you for taking the " + survey.getTitle() + " survey. Good Bye";
            if (request.getParameter("MessageSid") != null) {
                responseWriter.print(TwiMLUtil.messagingResponse(message));
            } else {
                responseWriter.print(TwiMLUtil.voiceResponse(message));
            }
        } else {
            responseWriter.print(TwiMLUtil.redirect(survey.getNextQuestionNumber(currentQuestion), survey));
        }
        response.setContentType("application/xml");
    }

    private void persistResponse(Response questionResponse) {
        Question currentQuestion = questionResponse.getQuestion();
        Response previousResponse = responseService.getBySessionSidAndQuestion(questionResponse.getSessionSid(), currentQuestion);
        if (previousResponse != null) {
            // it's already answered. That's an update from Twilio API (Transcriptions, for instance)
            questionResponse.setId(previousResponse.getId());
        }

        /** creates the question response on the db */
        responseService.save(questionResponse);
    }

    private Question getQuestionFromRequest(HttpServletRequest request) {
        return questionService.find(Long.parseLong(request.getParameter("qid")));
    }
}
src/main/java/com/twilio/survey/controllers/ResponseController.java
Process and store user survey responses

src/main/java/com/twilio/survey/controllers/ResponseController.java

Now, let's see how to visualize the results of our survey.

Displaying the Survey Results

For this route we simply query the database using a JPA query and then display the information within a Mustache template. We display a panel for every question in the survey, and inside each panel we list the responses from different calls.

You can access this page in the application's root route.

Loading Code Samples...
Language
package com.twilio.survey.controllers;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.ResponseRepository;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.ResponseService;
import com.twilio.survey.services.SurveyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

@Controller
public class DisplayController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;
    @Autowired
    private ResponseRepository responseRepository;
    private ResponseService responseService;

    public DisplayController() {
    }

    /**
     * Renders the survey results
     *
     * @param model    Empty model where you fill in the data that the template will use
     * @param request  Standard HttpServletRequest request
     * @param response Standard HttpServletResponse response
     * @return returns the template's name
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(Map<String, Object> model, HttpServletRequest request,
                        HttpServletResponse response) {
        this.surveyService = new SurveyService(surveyRepository);
        this.responseService = new ResponseService(responseRepository);

        Survey lastSurvey = surveyService.findLast();
        model.put("surveyTitle", lastSurvey.getTitle());

        List<Question> questions = lastSurvey.getQuestions();

        model.put("questions", questions);

        return "index";
    }
}
src/main/java/com/twilio/survey/controllers/DisplayController.java
Return survey results for template rendering

src/main/java/com/twilio/survey/controllers/DisplayController.java

That's it!

If you have configured one of your Twilio numbers to work with the application built in this tutorial, you should be able to take the survey and see the results under the root route of the application. We hope you found this sample application useful.

Where to next?

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

Appointment Reminders

Automate the process of reaching out to your customers in advance of an upcoming appointment.

Click to Call

Click-to-call enables your company to convert web traffic into phone calls with the click of a button.

Did this help?

Thanks for checking this tutorial out! If you have any feedback to share with us, we'd love to hear it. Connect with us on Twitter and let us know what you build!

Mario Celi
Jose Oliveros
Kat King
Andrew Baker
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.survey;

import com.twilio.survey.repositories.QuestionRepository;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.QuestionService;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.SurveyParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SurveyJavaApplication implements CommandLineRunner {
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private SurveyRepository surveyRepository;

    public static void main(String[] args) {
        SpringApplication.run(SurveyJavaApplication.class, args);
    }

    /**
     * Method that runs on app initialization. It will parse and insert the questions in the DB
     * on every app initialization
     */
    @Override
    public void run(String... strings) throws Exception {
        SurveyService surveyService = new SurveyService(surveyRepository);
        QuestionService questionService = new QuestionService(questionRepository);

        SurveyParser surveyParser = new SurveyParser(surveyService, questionService);
        surveyParser.parse("survey.json");
    }
}
package com.twilio.survey.util;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.survey.services.QuestionService;
import com.twilio.survey.services.SurveyService;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Date;

/**
 * Class in charge of parsing the JSON file that contains the list of questions.
 */
public class SurveyParser {
    SurveyService surveyService;
    QuestionService questionService;

    public SurveyParser() {
    }

    public SurveyParser(SurveyService surveyService, QuestionService questionService) {
        this.surveyService = surveyService;
        this.questionService = questionService;
    }

    /**
     * This method is called on app initialization.
     * It will insert the questions in the DB every time the application starts
     *
     * @param filePath path for the .json file
     */
    public void parse(String filePath) {
        FileReader reader = null;
        try {
            reader = new FileReader(filePath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        JSONParser jsonParser = new JSONParser();
        JSONObject jsonObject = null;
        try {
            jsonObject = (JSONObject) jsonParser.parse(reader);
        } catch (IOException e) {
            System.out.println("Error reading from survey file");
        } catch (ParseException e) {
            System.out.println("Error while parsing JSON survey file");
        }

        String title = (String) jsonObject.get("title");
        JSONArray questions = (JSONArray) jsonObject.get("questions");

        insertSurveyIntoDb(title, questions);
    }

    @Transactional
    private void insertSurveyIntoDb(String title, JSONArray questions) {
        Survey survey = new Survey(title, new Date());
        surveyService.create(survey);
        Question newQuestion;

        for (Object question : questions) {
            JSONObject obj = (JSONObject) question;
            String body = (String) obj.get("body");
            String type = (String) obj.get("type");
            newQuestion = new Question(body, type, survey, new Date());
            questionService.save(newQuestion);
        }
    }
}
package com.twilio.survey.controllers;

import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.TwiMLUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
public class SurveyController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;

    public SurveyController() {
    }

    /**
     * Calls endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * Otherwise it plays a message and hang up the call if there is no survey available.
     */
    @RequestMapping(value = "/survey/call", method = RequestMethod.GET)
    public void call(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();

        if (lastSurvey != null) {
            response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
        } else {
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    /**
     * SMS endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * As SMS is just a message instead of a long running call, we store state by mapping a Twilio's Cookie to a Session
     */
    @RequestMapping(value = "/survey/sms", method = RequestMethod.GET)
    public void sms(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();
        HttpSession session = request.getSession(false);

        if (lastSurvey != null) {
            if (session == null || session.isNew()) {
                // New session,
                response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
            } else {
                // Ongoing session, redirect to ResponseController to save it's answer.
                response.getWriter().print(getSaveResponseRedirect(session));
            }
        } else {
            // No survey
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    private String getSaveResponseRedirect(HttpSession session) throws Exception {
        String saveURL = "/save_response?qid=" + getQuestionIdFromSession(session);
        return TwiMLUtil.redirectPost(saveURL);
    }

    /**
     * Creates the TwiMLResponse for the first question of the survey
     *
     * @param survey  Survey entity
     * @param request HttpServletRequest request
     * @return TwiMLResponse
     */
    private String getFirstQuestionRedirect(Survey survey, HttpServletRequest request) throws Exception {
        String welcomeMessage = "Welcome to the " + survey.getTitle() + " survey";
        String questionURL = "/question?survey=" + survey.getId() + "&question=1";
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponseWithRedirect(welcomeMessage, questionURL);
        } else {
            return TwiMLUtil.voiceResponseWithRedirect(welcomeMessage, questionURL);
        }
    }

    /**
     * Creates a TwiMLResponse if no surveys are found on the database
     * For SMS, it's just a message
     * For Voice it should also send a Hangup to the ongoing call
     *
     * @return TwiMLResponse
     */
    private String getHangupResponse(HttpServletRequest request) throws Exception {
        String errorMessage = "We are sorry, there are no surveys available. Good bye.";
        cleanSession(request);
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponse(errorMessage);
        } else {
            return TwiMLUtil.voiceResponse(errorMessage);
        }
    }

    private void cleanSession(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
    }

    private Long getQuestionIdFromSession(HttpSession session) {
        return (Long) session.getAttribute("questionId");
    }
}
package com.twilio.survey.controllers;

import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.TwiMLUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
public class SurveyController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;

    public SurveyController() {
    }

    /**
     * Calls endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * Otherwise it plays a message and hang up the call if there is no survey available.
     */
    @RequestMapping(value = "/survey/call", method = RequestMethod.GET)
    public void call(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();

        if (lastSurvey != null) {
            response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
        } else {
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    /**
     * SMS endpoint; Welcomes a user and redirects to the question controller if there is a survey to be answered.
     * As SMS is just a message instead of a long running call, we store state by mapping a Twilio's Cookie to a Session
     */
    @RequestMapping(value = "/survey/sms", method = RequestMethod.GET)
    public void sms(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);

        Survey lastSurvey = surveyService.findLast();
        HttpSession session = request.getSession(false);

        if (lastSurvey != null) {
            if (session == null || session.isNew()) {
                // New session,
                response.getWriter().print(getFirstQuestionRedirect(lastSurvey, request));
            } else {
                // Ongoing session, redirect to ResponseController to save it's answer.
                response.getWriter().print(getSaveResponseRedirect(session));
            }
        } else {
            // No survey
            response.getWriter().print(getHangupResponse(request));
        }
        response.setContentType("application/xml");
    }

    private String getSaveResponseRedirect(HttpSession session) throws Exception {
        String saveURL = "/save_response?qid=" + getQuestionIdFromSession(session);
        return TwiMLUtil.redirectPost(saveURL);
    }

    /**
     * Creates the TwiMLResponse for the first question of the survey
     *
     * @param survey  Survey entity
     * @param request HttpServletRequest request
     * @return TwiMLResponse
     */
    private String getFirstQuestionRedirect(Survey survey, HttpServletRequest request) throws Exception {
        String welcomeMessage = "Welcome to the " + survey.getTitle() + " survey";
        String questionURL = "/question?survey=" + survey.getId() + "&question=1";
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponseWithRedirect(welcomeMessage, questionURL);
        } else {
            return TwiMLUtil.voiceResponseWithRedirect(welcomeMessage, questionURL);
        }
    }

    /**
     * Creates a TwiMLResponse if no surveys are found on the database
     * For SMS, it's just a message
     * For Voice it should also send a Hangup to the ongoing call
     *
     * @return TwiMLResponse
     */
    private String getHangupResponse(HttpServletRequest request) throws Exception {
        String errorMessage = "We are sorry, there are no surveys available. Good bye.";
        cleanSession(request);
        if (request.getParameter("MessageSid") != null) {
            return TwiMLUtil.messagingResponse(errorMessage);
        } else {
            return TwiMLUtil.voiceResponse(errorMessage);
        }
    }

    private void cleanSession(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
    }

    private Long getQuestionIdFromSession(HttpSession session) {
        return (Long) session.getAttribute("questionId");
    }
}
package com.twilio.survey.controllers;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.SurveyService;
import com.twilio.survey.util.QuestionBuilder;
import com.twilio.survey.util.SMSQuestionBuilder;
import com.twilio.survey.util.VoiceQuestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
public class QuestionController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;

    public QuestionController() {
    }

    /**
     * End point that returns the appropriate question response based on the parameters it receives
     */
    @RequestMapping(value = "/question", method = RequestMethod.GET, produces="application/xml")
    public void show(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.surveyService = new SurveyService(surveyRepository);
        Survey survey = surveyService.find(Long.parseLong(request.getParameter("survey")));

        Question currentQuestion = survey.getQuestionByNumber(Integer.parseInt(request.getParameter("question")));
        QuestionBuilder builder = getQuestionHandler(currentQuestion, request);

        if (currentQuestion != null) {
            response.getWriter().print(builder.build());
        } else {
            response.getWriter().print(builder.buildNoMoreQuestions());
        }
        response.setContentType("application/xml");
    }

    private void createSessionForQuestion(HttpServletRequest request, Question currentQuestion) {
        if (currentQuestion == null) {
            return;
        }
        HttpSession session = request.getSession(true);
        session.setAttribute("questionId", currentQuestion.getId());
    }

    private QuestionBuilder getQuestionHandler(Question currentQuestion, HttpServletRequest request) {
        if (isVoiceRequest(request)) {
            return new VoiceQuestionBuilder(currentQuestion);
        } else {
            createSessionForQuestion(request, currentQuestion);
            return new SMSQuestionBuilder(currentQuestion);
        }
    }

    private boolean isVoiceRequest(HttpServletRequest request) {
        return request.getParameter("MessageSid") == null;
    }
}
package com.twilio.survey.util;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.twiml.Body;
import com.twilio.twiml.Gather;
import com.twilio.twiml.Hangup;
import com.twilio.twiml.Message;
import com.twilio.twiml.MessagingResponse;
import com.twilio.twiml.Method;
import com.twilio.twiml.Record;
import com.twilio.twiml.Redirect;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiML;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

public class TwiMLUtil {

    public static String redirect(int nextQuestionNumber, Survey survey) throws TwiMLException {
        String nextQuestionURL = "/question?survey=" + survey.getId() + "&question=" + nextQuestionNumber;
        return redirect(nextQuestionURL, Method.GET).toXml();
    }

    public static String redirectPost(String url) throws TwiMLException {
        return redirect(url, Method.POST).toXml();
    }

    private static TwiML redirect(String url, Method method) {
        return new VoiceResponse.Builder()
                .redirect(new Redirect.Builder().url(url).method(method).build())
                .build();
    }

    public static Record record(Question question) {
        return new Record.Builder()
                .action("/save_response?qid=" + question.getId())
                .method(Method.POST)
                .transcribe(true)
                .transcribeCallback("/save_response?qid=" + question.getId())
                .maxLength(60)
                .build();
    }

    public static Gather gather(Question question) {
        return new Gather.Builder()
                .action("/save_response?qid=" + question.getId())
                .method(Method.POST)
                .finishOnKey("#")
                .build();
    }

    public static String voiceResponse(String message) throws TwiMLException {
        return new VoiceResponse.Builder()
                .say(new Say.Builder(message).build())
                .hangup(new Hangup())
                .build()
                .toXml();
    }

    public static String messagingResponse(String message) throws TwiMLException {
        return new MessagingResponse.Builder()
                .message(new Message.Builder().body(new Body(message)).build())
                .build()
                .toXml();
    }

    public static String voiceResponseWithRedirect(String message, String questionUrl) throws TwiMLException {
        return new VoiceResponse.Builder()
                .say(new Say.Builder(message).build())
                .redirect(new Redirect.Builder().url(questionUrl).method(Method.GET).build())
                .build()
                .toXml();
    }

    public static String messagingResponseWithRedirect(String message, String redirectUrl) throws TwiMLException {
        return new MessagingResponse.Builder()
                .message(new Message.Builder().body(new Body(message)).build())
                .redirect(new Redirect.Builder().url(redirectUrl).method(Method.GET).build())
                .build()
                .toXml();
    }
}
package com.twilio.survey.controllers;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Response;
import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.QuestionRepository;
import com.twilio.survey.repositories.ResponseRepository;
import com.twilio.survey.services.QuestionService;
import com.twilio.survey.services.ResponseService;
import com.twilio.survey.util.ResponseParser;
import com.twilio.survey.util.TwiMLUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Controller
public class ResponseController {
    @Autowired
    private QuestionRepository questionRepository;
    private QuestionService questionService;
    @Autowired
    private ResponseRepository responseRepository;
    private ResponseService responseService;

    public ResponseController() {
    }

    /**
     * End point that saves a question response and redirects the call to the next question,
     * if one is available.
     */
    @RequestMapping(value = "/save_response", method = RequestMethod.POST, produces="application/xml")
    public void save(HttpServletRequest request, HttpServletResponse response) throws Exception {
        PrintWriter responseWriter = response.getWriter();
        this.questionService = new QuestionService(questionRepository);
        this.responseService = new ResponseService(responseRepository);

        Question currentQuestion = getQuestionFromRequest(request);
        Survey survey = currentQuestion.getSurvey();
        persistResponse(new ResponseParser(currentQuestion, request).parse());

        if (survey.isLastQuestion(currentQuestion)) {
            String message = "Tank you for taking the " + survey.getTitle() + " survey. Good Bye";
            if (request.getParameter("MessageSid") != null) {
                responseWriter.print(TwiMLUtil.messagingResponse(message));
            } else {
                responseWriter.print(TwiMLUtil.voiceResponse(message));
            }
        } else {
            responseWriter.print(TwiMLUtil.redirect(survey.getNextQuestionNumber(currentQuestion), survey));
        }
        response.setContentType("application/xml");
    }

    private void persistResponse(Response questionResponse) {
        Question currentQuestion = questionResponse.getQuestion();
        Response previousResponse = responseService.getBySessionSidAndQuestion(questionResponse.getSessionSid(), currentQuestion);
        if (previousResponse != null) {
            // it's already answered. That's an update from Twilio API (Transcriptions, for instance)
            questionResponse.setId(previousResponse.getId());
        }

        /** creates the question response on the db */
        responseService.save(questionResponse);
    }

    private Question getQuestionFromRequest(HttpServletRequest request) {
        return questionService.find(Long.parseLong(request.getParameter("qid")));
    }
}
package com.twilio.survey.controllers;

import com.twilio.survey.models.Question;
import com.twilio.survey.models.Survey;
import com.twilio.survey.repositories.ResponseRepository;
import com.twilio.survey.repositories.SurveyRepository;
import com.twilio.survey.services.ResponseService;
import com.twilio.survey.services.SurveyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

@Controller
public class DisplayController {
    @Autowired
    private SurveyRepository surveyRepository;
    private SurveyService surveyService;
    @Autowired
    private ResponseRepository responseRepository;
    private ResponseService responseService;

    public DisplayController() {
    }

    /**
     * Renders the survey results
     *
     * @param model    Empty model where you fill in the data that the template will use
     * @param request  Standard HttpServletRequest request
     * @param response Standard HttpServletResponse response
     * @return returns the template's name
     */
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(Map<String, Object> model, HttpServletRequest request,
                        HttpServletResponse response) {
        this.surveyService = new SurveyService(surveyRepository);
        this.responseService = new ResponseService(responseRepository);

        Survey lastSurvey = surveyService.findLast();
        model.put("surveyTitle", lastSurvey.getTitle());

        List<Question> questions = lastSurvey.getQuestions();

        model.put("questions", questions);

        return "index";
    }
}