One of the more abstract concepts you'll handle when building your business is what the workflow will look like.
At its core, setting up a standardized workflow is about enabling your service providers (agents, hosts, customer service reps, administrators, and the rest of the gang) to better serve your customers.
To illustrate a very real-world example, today we'll build a Java and Servlets webapp for finding and booking vacation properties — tentatively called Airtng.
Here's how it'll work:
We'll be using the Twilio REST API to send our users messages at important junctures. Here's a bit more on our API:
For this workflow to work, we need to have a user model and allow logins.
src/main/java/org/twilio/airtng/models/User.java
1package org.twilio.airtng.models;23import javax.persistence.*;4import java.util.ArrayList;5import java.util.List;67@Entity8@Table(name = "users")9public class User {10@Id11@GeneratedValue(strategy = GenerationType.IDENTITY)12@Column(name = "id")13private long id;1415@Column(name = "name")16private String name;1718@Column(name = "email")19private String email;2021@Column(name = "password")22private String password;2324@Column(name = "phone_number")25private String phoneNumber;2627@OneToMany(mappedBy="user")28private List<Reservation> reservations;2930@OneToMany(mappedBy="user")31private List<VacationProperty> vacationProperties;3233public User() {34this.reservations = new ArrayList<>();35this.vacationProperties = new ArrayList<>();36}3738public User(39String name,40String email,41String password,42String phoneNumber) {43this();44this.name = name;45this.email = email;46this.password = password;47this.phoneNumber = phoneNumber;48}4950public long getId() {51return id;52}5354public void setId(long id) {55this.id = id;56}5758public String getName() {59return name;60}6162public void setName(String name) {63this.name = name;64}6566public String getEmail() {67return email;68}6970public void setEmail(String email) {71this.email = email;72}7374public String getPassword() {75return password;76}7778public void setPassword(String password) {79this.password = password;80}8182public String getPhoneNumber() {83return phoneNumber;84}8586public void setPhoneNumber(String phoneNumber) {87this.phoneNumber = phoneNumber;88}8990public void addReservation(Reservation reservation) {9192if (this.reservations.add(reservation) && reservation.getUser() != this) {93reservation.setUser(this);94}95}9697public void removeReservation(Reservation reservation) {98if (this.reservations.remove(reservation) && reservation.getUser() == this) {99reservation.setUser(null);100}101}102103public void addVacationProperty(VacationProperty vacationProperty) {104105if (this.vacationProperties.add(vacationProperty) && vacationProperty.getUser() != this) {106vacationProperty.setUser(this);107}108}109110public void removeVacationProperty(VacationProperty vacationProperty) {111if (this.reservations.remove(vacationProperty) && vacationProperty.getUser() == this) {112vacationProperty.setUser(null);113}114}115116}
Next, let's look at the Vacation Property model.
In order to build a true vacation rental company we'll need a way to create a property rental listing.
The VacationProperty
model belongs to the User
who created it (we'll call this user the host moving forward) and contains only two properties: a description
and an imageUrl
.
The model has two associations implemented: there are many reservations in it and many users can make those reservations.
src/main/java/org/twilio/airtng/models/VacationProperty.java
1package org.twilio.airtng.models;23import javax.persistence.*;4import java.util.ArrayList;5import java.util.List;67@Entity8@Table(name = "vacation_properties")9public class VacationProperty {1011@javax.persistence.Id12@GeneratedValue(strategy = GenerationType.IDENTITY)13@Column(name = "id")14private long id;1516@Column(name = "description")17private String description;1819@Column(name = "image_url")20private String imageUrl;2122@OneToMany(mappedBy = "vacationProperty")23private List<Reservation> reservations;2425@ManyToOne(fetch = FetchType.LAZY)26@JoinColumn(name = "user_id")27private User user;2829public VacationProperty() {30this.reservations = new ArrayList<Reservation>();31}3233public VacationProperty(String description, String imageUrl, User user) {34this.description = description;35this.imageUrl = imageUrl;36this.user = user;37}3839public long getId() {40return id;41}4243public void setId(long id) {44this.id = id;45}4647public String getDescription() {48return description;49}5051public void setDescription(String description) {52this.description = description;53}5455public String getImageUrl() {56return imageUrl;57}5859public void setImageUrl(String imageUrl) {60this.imageUrl = imageUrl;61}6263public User getUser() {64return this.user;65}6667public void setUser(User user) {68this.user = user;69}7071public void addReservation(Reservation reservation) {7273if (this.reservations.add(reservation) && reservation.getVacationProperty() != this) {74reservation.setVacationProperty(this);75}76}7778public void removeReservation(Reservation reservation) {79if (this.reservations.remove(reservation) && reservation.getVacationProperty() == this) {80reservation.setVacationProperty(null);81}82}83}
Next, let's look at the all-important Reservation model.
The Reservation
model is at the center of the workflow for this application. It is responsible for keeping track of:
guest
who performed the reservationvacation property
the guest is requesting (and associated host)status
of the reservation: pending
, confirmed
, or rejected
src/main/java/org/twilio/airtng/models/Reservation.java
1package org.twilio.airtng.models;23import javax.persistence.*;45@Entity6@Table(name = "reservations")7public class Reservation {89@javax.persistence.Id10@GeneratedValue(strategy = GenerationType.IDENTITY)11@Column(name = "id")12private long id;1314@Column(name = "message")15private String message;1617@Column(name = "status")18private Integer status;1920@ManyToOne(fetch = FetchType.LAZY)21@JoinColumn(name = "user_id")22private User user;2324@ManyToOne(fetch = FetchType.LAZY)25@JoinColumn(name = "vacation_property_id")26private VacationProperty vacationProperty;2728public Reservation() {29}3031public Reservation(String message, VacationProperty vacationProperty, User user) {32this();33this.message = message;34this.vacationProperty = vacationProperty;35this.user = user;36this.setStatus(Status.Pending);37}3839public long getId() {40return id;41}4243public void setId(long id) {44this.id = id;45}4647public String getMessage() {48return message;49}5051public void setMessage(String message) {52this.message = message;53}5455public Status getStatus() {56return Status.getType(this.status);57}5859public void setStatus(Status status) {60if (status == null) {61this.status = null;62} else {63this.status = status.getValue();64}65}6667public VacationProperty getVacationProperty() {68return vacationProperty;69}7071public void setVacationProperty(VacationProperty vacationProperty) {72this.vacationProperty = vacationProperty;73}7475public User getUser() {76return this.user;77}7879public void setUser(User user) {80this.user = user;81}8283public void reject() {84this.setStatus(Status.Rejected);85}8687public void confirm() {88this.setStatus(Status.Confirmed);89}9091public enum Status {9293Pending(0), Confirmed(1), Rejected(2);9495private int value;9697Status(int value) {98this.value = value;99}100101public int getValue() {102return value;103}104105@Override106public String toString() {107switch (value) {108case 0:109return "pending";110case 1:111return "confirmed";112case 2:113return "rejected";114}115return "Value out of range";116}117118119public static Status getType(Integer id) {120121if (id == null) {122return null;123}124125for (Status status : Status.values()) {126if (id.equals(status.getValue())) {127return status;128}129}130throw new IllegalArgumentException("No matching type for value " + id);131}132}133}
Next up, we'll show how our new application will create a new reservation.
The reservation creation form holds only one field: the message that will be sent to the host when reserving one of her properties.
The rest of the information necessary to create a reservation is taken from the logged in user and the relationship between a property and its owner. Our base generic Repository
is in charge of handling entity insertion.
A reservation is created with a default status of pending
. This lets our application react to a host rejecting or accepting a reservation request.
src/main/java/org/twilio/airtng/servlets/ReservationServlet.java
1package org.twilio.airtng.servlets;23import org.twilio.airtng.lib.notifications.SmsNotifier;4import org.twilio.airtng.lib.servlets.WebAppServlet;5import org.twilio.airtng.lib.web.request.validators.RequestParametersValidator;6import org.twilio.airtng.models.Reservation;7import org.twilio.airtng.models.User;8import org.twilio.airtng.models.VacationProperty;9import org.twilio.airtng.repositories.ReservationRepository;10import org.twilio.airtng.repositories.UserRepository;11import org.twilio.airtng.repositories.VacationPropertiesRepository;1213import javax.servlet.ServletException;14import javax.servlet.http.HttpServletRequest;15import javax.servlet.http.HttpServletResponse;16import java.io.IOException;1718public class ReservationServlet extends WebAppServlet {1920private final VacationPropertiesRepository vacationPropertiesRepository;21private final ReservationRepository reservationRepository;22private UserRepository userRepository;23private SmsNotifier smsNotifier;2425public ReservationServlet() {26this(new VacationPropertiesRepository(), new ReservationRepository(), new UserRepository(), new SmsNotifier());27}2829public ReservationServlet(VacationPropertiesRepository vacationPropertiesRepository, ReservationRepository reservationRepository, UserRepository userRepository, SmsNotifier smsNotifier) {30super();31this.vacationPropertiesRepository = vacationPropertiesRepository;32this.reservationRepository = reservationRepository;33this.userRepository = userRepository;34this.smsNotifier = smsNotifier;35}3637@Override38public void doGet(HttpServletRequest request, HttpServletResponse response)39throws ServletException, IOException {4041VacationProperty vacationProperty = vacationPropertiesRepository.find(Long.parseLong(request.getParameter("id")));42request.setAttribute("vacationProperty", vacationProperty);43request.getRequestDispatcher("/reservation.jsp").forward(request, response);44}4546@Override47public void doPost(HttpServletRequest request, HttpServletResponse response)48throws ServletException, IOException {4950super.doPost(request, response);5152String message = null;53VacationProperty vacationProperty = null;5455if (isValidRequest()) {56message = request.getParameter("message");57String propertyId = request.getParameter("propertyId");58vacationProperty = vacationPropertiesRepository.find(Long.parseLong(propertyId));5960User currentUser = userRepository.find(sessionManager.get().getLoggedUserId(request));61Reservation reservation = reservationRepository.create(new Reservation(message, vacationProperty, currentUser));62smsNotifier.notifyHost(reservation);63response.sendRedirect("/properties");64}65preserveStatusRequest(request, message, vacationProperty);66request.getRequestDispatcher("/reservation.jsp").forward(request, response);67}6869@Override70protected boolean isValidRequest(RequestParametersValidator validator) {7172return validator.validatePresence("message");73}7475private void preserveStatusRequest(76HttpServletRequest request,77String message, Object vacationProperty) {78request.setAttribute("message", message);79request.setAttribute("vacationProperty", vacationProperty);80}81}
Now let's look at how the system will notify a host
when a new reservation request is submitted.
When a reservation is created for a property we want to notify the owner that someone has made a reservation.
We use an abstraction called SmsNotifier
which under the hood uses another abstraction called Sender
. Here is where we use Twilio's Rest API to send an SMS message to the host using your Twilio phone number.
src/main/java/org/twilio/airtng/lib/sms/Sender.java
1package org.twilio.airtng.lib.sms;23import com.twilio.http.TwilioRestClient;4import com.twilio.rest.api.v2010.account.MessageCreator;5import com.twilio.type.PhoneNumber;6import org.twilio.airtng.lib.Config;78public class Sender {910private final TwilioRestClient client;1112public Sender() {13client = new TwilioRestClient.Builder(Config.getAccountSid(), Config.getAuthToken()).build();14}1516public Sender(TwilioRestClient client) {17this.client = client;18}1920public void send(String to, String message) {21new MessageCreator(22new PhoneNumber(to),23new PhoneNumber(Config.getPhoneNumber()),24message25).create(client);26}2728}
The next step shows how to handle a host accepting or rejecting a request - let's continue.
Let's take a closer look at the ReservationConfirmation
servlet. This servlet handles our incoming Twilio request and does three things:
In the Twilio console, you should change the 'A Message Comes In' webhook to call your application's endpoint in the route /reservation-confirmation:
One way to expose your machine to the world during development is to use ngrok. Your URL for the SMS web hook on your phone number should look something like this:
http://<subdomain>.ngrok.io/reservation-confirmation
An incoming request from Twilio comes with some helpful parameters, including the From
phone number and the message Body
.
We'll use the From
parameter to lookup the host and check if they have any pending reservations. If they do, we'll use the message body to check for an 'accept' or 'reject'.
Finally, we update the reservation status and use the SmsNotifier
abstraction to send an SMS to the guest with the news.
Webhook for handling Host's decision
1package org.twilio.airtng.servlets;23import com.twilio.twiml.MessagingResponse;4import com.twilio.twiml.TwiMLException;5import org.twilio.airtng.lib.helpers.TwiMLHelper;6import org.twilio.airtng.lib.notifications.SmsNotifier;7import org.twilio.airtng.lib.servlets.WebAppServlet;8import org.twilio.airtng.models.Reservation;9import org.twilio.airtng.models.User;10import org.twilio.airtng.repositories.ReservationRepository;11import org.twilio.airtng.repositories.UserRepository;1213import javax.servlet.ServletException;14import javax.servlet.http.HttpServletRequest;15import javax.servlet.http.HttpServletResponse;16import java.io.IOException;1718public class ReservationConfirmationServlet extends WebAppServlet {1920private UserRepository userRepository;21private ReservationRepository reservationRepository;22private SmsNotifier smsNotifier;2324public ReservationConfirmationServlet() {25this(new UserRepository(), new ReservationRepository(), new SmsNotifier());26}2728public ReservationConfirmationServlet(UserRepository userRepository, ReservationRepository reservationRepository, SmsNotifier smsNotifier) {29super();30this.userRepository = userRepository;31this.reservationRepository = reservationRepository;32this.smsNotifier = smsNotifier;33}3435@Override36public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {3738String phone = request.getParameter("From");39String smsContent = request.getParameter("Body");4041String smsResponseText = "Sorry, it looks like you don't have any reservations to respond to.";4243try {44User user = userRepository.findByPhoneNumber(phone);45Reservation reservation = reservationRepository.findFirstPendantReservationsByUser(user.getId());46if (reservation != null) {47if (smsContent.contains("yes") || smsContent.contains("accept"))48reservation.confirm();49else50reservation.reject();51reservationRepository.update(reservation);5253smsResponseText = String.format("You have successfully %s the reservation", reservation.getStatus().toString());54smsNotifier.notifyGuest(reservation);55}5657respondSms(response, smsResponseText);5859} catch (Exception e) {60throw new RuntimeException(e);61}62}6364private void respondSms(HttpServletResponse response, String message)65throws IOException, TwiMLException {66MessagingResponse twiMLResponse = TwiMLHelper.buildSmsRespond(message);67response.setContentType("text/xml");68response.getWriter().write(twiMLResponse.toXml());69}70}
Next up, let's see how we will respond to Twilio.
Finally, we'll use Twilio's TwiML as a response to Twilio's server instructing it to send SMS notification message to the host.
We'll be using the Message
verb to instruct Twilio's server that it should send the message.
src/main/java/org/twilio/airtng/servlets/ReservationConfirmationServlet.java
1package org.twilio.airtng.servlets;23import com.twilio.twiml.MessagingResponse;4import com.twilio.twiml.TwiMLException;5import org.twilio.airtng.lib.helpers.TwiMLHelper;6import org.twilio.airtng.lib.notifications.SmsNotifier;7import org.twilio.airtng.lib.servlets.WebAppServlet;8import org.twilio.airtng.models.Reservation;9import org.twilio.airtng.models.User;10import org.twilio.airtng.repositories.ReservationRepository;11import org.twilio.airtng.repositories.UserRepository;1213import javax.servlet.ServletException;14import javax.servlet.http.HttpServletRequest;15import javax.servlet.http.HttpServletResponse;16import java.io.IOException;1718public class ReservationConfirmationServlet extends WebAppServlet {1920private UserRepository userRepository;21private ReservationRepository reservationRepository;22private SmsNotifier smsNotifier;2324public ReservationConfirmationServlet() {25this(new UserRepository(), new ReservationRepository(), new SmsNotifier());26}2728public ReservationConfirmationServlet(UserRepository userRepository, ReservationRepository reservationRepository, SmsNotifier smsNotifier) {29super();30this.userRepository = userRepository;31this.reservationRepository = reservationRepository;32this.smsNotifier = smsNotifier;33}3435@Override36public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {3738String phone = request.getParameter("From");39String smsContent = request.getParameter("Body");4041String smsResponseText = "Sorry, it looks like you don't have any reservations to respond to.";4243try {44User user = userRepository.findByPhoneNumber(phone);45Reservation reservation = reservationRepository.findFirstPendantReservationsByUser(user.getId());46if (reservation != null) {47if (smsContent.contains("yes") || smsContent.contains("accept"))48reservation.confirm();49else50reservation.reject();51reservationRepository.update(reservation);5253smsResponseText = String.format("You have successfully %s the reservation", reservation.getStatus().toString());54smsNotifier.notifyGuest(reservation);55}5657respondSms(response, smsResponseText);5859} catch (Exception e) {60throw new RuntimeException(e);61}62}6364private void respondSms(HttpServletResponse response, String message)65throws IOException, TwiMLException {66MessagingResponse twiMLResponse = TwiMLHelper.buildSmsRespond(message);67response.setContentType("text/xml");68response.getWriter().write(twiMLResponse.toXml());69}70}
Congrats! You've just learned how to automate your workflow with Twilio SMS.
Next, let's look at some other interesting features you might want to add to your application.
Like Twilio? Like Java? You're in the right place. Here are a couple other excellent tutorials:
Build a server notification system that will alert all administrators via SMS when a server outage occurs.
Convert web traffic into phone calls with the click of a button.