Employee Directory with Java and Servlets

January 10, 2017
Written by
Eliecer Hernandez
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Paul Kamp
Twilion
Kat King
Twilion
Jose Oliveros
Contributor
Opinions expressed by Twilio contributors are their own

employee-directory-java-servlets

Learn how to implement an employee directory that you can query using SMS. Request information from anyone at your company just by sending a text message to a Twilio Number

Here is how it works at a high level:

  • The user sends a SMS with an Employee's name to the Twilio number.
  • The user receives information for the requested Employee.

Handle Twilio's SMS Request

When your Twilio Number receives an SMS, Twilio will make a POST request to /directory/search asking for TwiML instructions.

Once the application identifies one of the 3 possible scenarios (single partial match, multiple partial match or no match), it will send a TwiML response to Twilio. This response will instruct Twilio to send an SMS Message back to the user.

Editor: this is a migrated tutorial. Find the original code by cloning https://github.com/TwilioDevEd/employee-directory-servlets

package com.twilio.employeedirectory.application.servlet;

import com.twilio.employeedirectory.domain.common.Twilio;
import com.twilio.employeedirectory.domain.common.Utils;
import com.twilio.employeedirectory.domain.error.EmployeeLoadException;
import com.twilio.employeedirectory.domain.query.EmployeeMatch;
import com.twilio.employeedirectory.domain.query.MultipleMatch;
import com.twilio.employeedirectory.domain.query.NoMatch;
import com.twilio.employeedirectory.domain.service.EmployeeDirectoryService;
import com.twilio.twiml.MessagingResponse;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.messaging.Body;
import com.twilio.twiml.messaging.Message;
import org.apache.http.NameValuePair;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

@Singleton
public class EmployeeDirectoryServlet extends HttpServlet {

  private static final Logger LOG = Logger.getLogger(EmployeeDirectoryServlet.class.getName());

  public static final String SUGGESTIONS_COOKIE_NAME = "last-query";

  private final EmployeeDirectoryService employeeDirectoryService;

  @Inject
  public EmployeeDirectoryServlet(final EmployeeDirectoryService employeeDirectoryService) {
    this.employeeDirectoryService = employeeDirectoryService;
  }

  public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    Optional<String> fullNameQuery = Optional.ofNullable(request.getParameter(Twilio.QUERY_PARAM));
    try {
      EmployeeMatch matchResponse =
          fullNameQuery
            .map(queryValue -> createEmployeeMatchFromRequest(request, response, queryValue))
            .orElse(new NoMatch());
      request.setAttribute("employeeMatch", matchResponse);
      printMatch(response, matchResponse);
    } catch (EmployeeLoadException ex) {
      printError(response, ex.getMessage());
    }
  }

  private void saveEmployeeSuggestionsIntoCookie(MultipleMatch matchResponse,
                                                 HttpServletResponse response) {
    Cookie lastQueryCookie =
      new Cookie(SUGGESTIONS_COOKIE_NAME, matchResponse.getEmployeeSuggestions());
    response.addCookie(lastQueryCookie);
  }

  private EmployeeMatch createEmployeeMatchFromRequest(HttpServletRequest request,
                                                       HttpServletResponse response,
                                                       String queryValue) {
    List<NameValuePair> employeeSuggestions =
      Utils.getCookieAndDispose(response, SUGGESTIONS_COOKIE_NAME, request.getCookies());
    return employeeDirectoryService.queryEmployee(queryValue, employeeSuggestions);
  }

  private void printMatch(HttpServletResponse response, EmployeeMatch matchResponse) {
    try {
      // Only MultiplePartialMatch caches its response in a cookie
      if (matchResponse instanceof MultipleMatch) {
        saveEmployeeSuggestionsIntoCookie((MultipleMatch) matchResponse, response);
      }
      response.setContentType("text/xml");
      response.getWriter().print(matchResponse.getMessageTwiml());
    } catch (TwiMLException e) {
      throw new EmployeeLoadException("Invalid TwiML response");
    } catch (IOException e) {
      throw new EmployeeLoadException("Error writing response in the server");
    }
  }

  private void printError(HttpServletResponse response, String message) {
    try {
      response.setContentType("text/xml");
      MessagingResponse messagingResponse = new MessagingResponse.Builder()
        .message(new Message.Builder()
          .body(new Body.Builder(message).build())
          .build()
        ).build();
      response.getWriter().print(messagingResponse.toXml());
    } catch (IOException e) {
      LOG.log(Level.SEVERE, "Error trying to print a text message in the servlet");
    } catch (TwiMLException e) {
      LOG.log(Level.SEVERE, "Error trying to print a TwiML message in the servlet");
    }
  }
}

Let's take a closer look at each one of the scenarios.

Find a Single Employee Match

This is the simplest scenario. We query our database expecting to find an employee whose name is whole or partially like the one specified in the SMS sent to our Twilio number. If a single match is found, a message containing this employee's information is built and sent back to Twilio as TwiML instructions.

package com.twilio.employeedirectory.domain.query;

import com.twilio.employeedirectory.domain.model.Employee;
import com.twilio.twiml.MessagingResponse;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.messaging.Body;
import com.twilio.twiml.messaging.Media;
import com.twilio.twiml.messaging.Message;


/**
 * When it returns an {@link com.twilio.employeedirectory.domain.model.Employee}
 * which name is exactly the one queried.
 */
public class PerfectMatch implements EmployeeMatch {

  private final Employee foundEmployee;

  public PerfectMatch(Employee foundEmployee) {
    this.foundEmployee = foundEmployee;
  }

  @Override
  public String getMessage() {
    return String.format("%s\n%s\n%s", foundEmployee.getFullName(), foundEmployee.getPhoneNumber(),
        foundEmployee.getEmail());
  }

  @Override
  public String getMessageTwiml() throws TwiMLException {
    return new MessagingResponse.Builder()
      .message(new Message.Builder()
        .body(new Body.Builder(getMessage()).build())
        .media(new Media.Builder(foundEmployee.getImageUrl()).build())
        .build()
      )
      .build()
      .toXml();
  }

  public Employee getFoundEmployee() {
    return foundEmployee;
  }

  @Override
  public boolean isSingleEmployeeFound() {
    return true;
  }
}

If multiple matches are found we'll try to do a multiple partial match. That is our next possible scenario.

Find Multiple Matches

Now we'll try to get a partial match that returns more than one result. We need to store a List of BasicNameValuePair containing indexed suggestions so the user can reply with a number that references one of the suggestions in order to get all the employee's information. We'll use Twilio Cookies to store the suggestions.

package com.twilio.employeedirectory.domain.query;

import com.twilio.employeedirectory.domain.model.Employee;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;

import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.IntStream;

/**
 * When the response returns multiple results which are not perfect match of the
 * queried name
 */
public class MultipleMatch implements EmployeeMatch {

  private final List<Employee> foundEmployees;

  private String lastQueryId;

  public MultipleMatch(List<Employee> foundEmployees) {
    this.foundEmployees = foundEmployees;
  }

  @Override
  public String getMessage() {
    StringBuilder builder = new StringBuilder();
    IntStream.rangeClosed(1, foundEmployees.size()).boxed()
        .map(i -> String.format("%d %s\n", i, foundEmployees.get(i - 1))).forEach(builder::append);
    return builder.toString();
  }

  public List<Employee> getFoundEmployees() {
    return foundEmployees;
  }

  /**
   * Returns a url query string with indexed suggestions of
   * {@link EmployeeMatch}
   *
   * @return {@link String} not <code>null</code>
   */
  public String getEmployeeSuggestions() {
    if (lastQueryId == null) {
      List<BasicNameValuePair> params = new LinkedList<>();
      IntStream
          .rangeClosed(1, foundEmployees.size())
          .boxed()
          .map(
              i -> new BasicNameValuePair(i.toString(), foundEmployees.get(i - 1).getId()
                  .toString())).forEach(params::add);
      lastQueryId = URLEncodedUtils.format(params, StandardCharsets.US_ASCII);
    }
    return lastQueryId;
  }
}

Now you know how to attempt to find employee matches. Next, we will see how to handle the case where a match can't be found.

What if no Employee?

If none of the previous scenarios occur, it means that there is no employee in the database that matches the user's query. In that case, a response will be sent to the user explaining that their query has no results.

package com.twilio.employeedirectory.domain.query;

/**
 * When there's no match for the query
 */
public class NoMatch implements EmployeeMatch {
  @Override
  public String getMessage() {
    return "We did not find the employee you're looking for";
  }
}

We've built the search capability for our server. Next, we'll see how we use cookies to cache search suggestions.

Store Suggestions With Cookies

When a user gets multiple matches by searching the employee's directory, we reply with multiple indexed suggestions. We need to store these suggestions, so next time the user sends an SMS we know this is not a query for a new employee, but a selection of one of the suggestions.

We'll use Twilio Cookies to store suggestions. They will allow you to keep track of an SMS conversation between multiple numbers and your Twilio-powered application.

package com.twilio.employeedirectory.application.servlet;

import com.twilio.employeedirectory.domain.common.Twilio;
import com.twilio.employeedirectory.domain.common.Utils;
import com.twilio.employeedirectory.domain.error.EmployeeLoadException;
import com.twilio.employeedirectory.domain.query.EmployeeMatch;
import com.twilio.employeedirectory.domain.query.MultipleMatch;
import com.twilio.employeedirectory.domain.query.NoMatch;
import com.twilio.employeedirectory.domain.service.EmployeeDirectoryService;
import com.twilio.twiml.MessagingResponse;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.messaging.Body;
import com.twilio.twiml.messaging.Message;
import org.apache.http.NameValuePair;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

@Singleton
public class EmployeeDirectoryServlet extends HttpServlet {

  private static final Logger LOG = Logger.getLogger(EmployeeDirectoryServlet.class.getName());

  public static final String SUGGESTIONS_COOKIE_NAME = "last-query";

  private final EmployeeDirectoryService employeeDirectoryService;

  @Inject
  public EmployeeDirectoryServlet(final EmployeeDirectoryService employeeDirectoryService) {
    this.employeeDirectoryService = employeeDirectoryService;
  }

  public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    Optional<String> fullNameQuery = Optional.ofNullable(request.getParameter(Twilio.QUERY_PARAM));
    try {
      EmployeeMatch matchResponse =
          fullNameQuery
            .map(queryValue -> createEmployeeMatchFromRequest(request, response, queryValue))
            .orElse(new NoMatch());
      request.setAttribute("employeeMatch", matchResponse);
      printMatch(response, matchResponse);
    } catch (EmployeeLoadException ex) {
      printError(response, ex.getMessage());
    }
  }

  private void saveEmployeeSuggestionsIntoCookie(MultipleMatch matchResponse,
                                                 HttpServletResponse response) {
    Cookie lastQueryCookie =
      new Cookie(SUGGESTIONS_COOKIE_NAME, matchResponse.getEmployeeSuggestions());
    response.addCookie(lastQueryCookie);
  }

  private EmployeeMatch createEmployeeMatchFromRequest(HttpServletRequest request,
                                                       HttpServletResponse response,
                                                       String queryValue) {
    List<NameValuePair> employeeSuggestions =
      Utils.getCookieAndDispose(response, SUGGESTIONS_COOKIE_NAME, request.getCookies());
    return employeeDirectoryService.queryEmployee(queryValue, employeeSuggestions);
  }

  private void printMatch(HttpServletResponse response, EmployeeMatch matchResponse) {
    try {
      // Only MultiplePartialMatch caches its response in a cookie
      if (matchResponse instanceof MultipleMatch) {
        saveEmployeeSuggestionsIntoCookie((MultipleMatch) matchResponse, response);
      }
      response.setContentType("text/xml");
      response.getWriter().print(matchResponse.getMessageTwiml());
    } catch (TwiMLException e) {
      throw new EmployeeLoadException("Invalid TwiML response");
    } catch (IOException e) {
      throw new EmployeeLoadException("Error writing response in the server");
    }
  }

  private void printError(HttpServletResponse response, String message) {
    try {
      response.setContentType("text/xml");
      MessagingResponse messagingResponse = new MessagingResponse.Builder()
        .message(new Message.Builder()
          .body(new Body.Builder(message).build())
          .build()
        ).build();
      response.getWriter().print(messagingResponse.toXml());
    } catch (IOException e) {
      LOG.log(Level.SEVERE, "Error trying to print a text message in the servlet");
    } catch (TwiMLException e) {
      LOG.log(Level.SEVERE, "Error trying to print a TwiML message in the servlet");
    }
  }
}

That's it! We have just implemented an employee directory using Java with servlets. Now you can get your employee's information by texting a Twilio number.

Where to next?

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

Browser-Calls

Learn how to use Twilio Client to make browser-to-phone and browser-to-browser calls with ease.

ETA-Notifications

Learn how to implement ETA Notifications using Java with servlets and Twilio.

Did this help?

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