Ahoy! We now recommend you build your appointment reminders with Twilio's built in Message Scheduling functionality. Head on over to the Message Scheduling documentation to learn more about scheduling messages!
This is a Java 8 web application written using Spark that demonstrates how to send appointment reminders to your customers with Twilio SMS.
Check out this application on GitHub to download the code and read instructions on how to run it yourself. In this tutorial, we'll show you the key bits of code necessary to drive this use case.
Check out how Yelp uses SMS to confirm restaurant reservations for diners.
Let's get started! Click the button below to move on to the next step of the tutorial.
The Quartz scheduler is instantiated in the main method of our web application, before we set up the routes. We pass a reference to this scheduler to the controller so it can schedule jobs to send out appointment reminders. Note that by default, Quartz temporarily stores jobs in memory, but in production you can configure Quartz to store jobs in a data store of your choice.
src/main/java/com/twilio/appointmentreminders/Server.java
_69package com.twilio.appointmentreminders;_69_69import com.twilio.appointmentreminders.controllers.AppointmentController;_69import com.twilio.appointmentreminders.models.AppointmentService;_69import com.twilio.appointmentreminders.util.AppSetup;_69import com.twilio.appointmentreminders.util.LoggingFilter;_69import org.quartz.Scheduler;_69import org.quartz.SchedulerException;_69import org.quartz.impl.StdSchedulerFactory;_69import spark.Spark;_69import spark.template.mustache.MustacheTemplateEngine;_69_69import javax.persistence.EntityManagerFactory;_69_69import static spark.Spark.*;_69_69/**_69 * Main application class. The environment is set up here, and all necessary services are run._69 */_69public class Server {_69 public static void main(String[] args) {_69 AppSetup appSetup = new AppSetup();_69_69 /**_69 * Sets the port in which the application will run. Takes the port value from PORT_69 * environment variable, if not set, uses Spark default port 4567._69 */_69 port(appSetup.getPortNumber());_69_69 /**_69 * Gets the entity manager based on environment variable DATABASE_URL and injects it into_69 * AppointmentService which handles all DB operations._69 */_69 EntityManagerFactory factory = appSetup.getEntityManagerFactory();_69 AppointmentService service = new AppointmentService(factory.createEntityManager());_69_69 /**_69 * Specifies the directory within resources that will be publicly available when the_69 * application is running. Place static web files in this directory (JS, CSS)._69 */_69 Spark.staticFileLocation("/public");_69_69 /** Creates a new instance of Quartz Scheduler and starts it. */_69 Scheduler scheduler = null;_69 try {_69 scheduler = StdSchedulerFactory.getDefaultScheduler();_69_69 scheduler.start();_69_69 } catch (SchedulerException se) {_69 System.out.println("Unable to start scheduler service");_69 }_69_69 /** Injects AppointmentService and Scheduler into the controller. */_69 AppointmentController controller = new AppointmentController(service, scheduler);_69_69 /**_69 * Defines all url paths for the application and assigns a controller method for each._69 * If the route renders a page, the templating engine must be specified, and the controller_69 * should return the appropriate Route object._69 */_69 get("/", controller.index, new MustacheTemplateEngine());_69 get("/new", controller.renderCreatePage, new MustacheTemplateEngine());_69 post("/create", controller.create, new MustacheTemplateEngine());_69 post("/delete", controller.delete);_69_69 afterAfter(new LoggingFilter());_69 }_69}
Next let's see how we create a new Appointment
.
Once validations pass and the appointment is persisted to the database.
With scheduleJob
a notification is scheduled based on the time of the appointment.
src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java
_147package com.twilio.appointmentreminders.controllers;_147_147import com.twilio.appointmentreminders.models.Appointment;_147import com.twilio.appointmentreminders.models.AppointmentService;_147import com.twilio.appointmentreminders.util.AppointmentScheduler;_147import com.twilio.appointmentreminders.util.FieldValidator;_147import com.twilio.appointmentreminders.util.TimeZones;_147import org.joda.time.DateTime;_147import org.joda.time.DateTimeZone;_147import org.joda.time.format.DateTimeFormat;_147import org.joda.time.format.DateTimeFormatter;_147import org.quartz.JobDetail;_147import org.quartz.Scheduler;_147import org.quartz.SchedulerException;_147import org.quartz.Trigger;_147import spark.ModelAndView;_147import spark.Route;_147import spark.TemplateViewRoute;_147_147import java.util.Date;_147import java.util.HashMap;_147import java.util.List;_147import java.util.Map;_147_147import static org.quartz.JobBuilder.newJob;_147import static org.quartz.TriggerBuilder.newTrigger;_147_147/**_147 * Appointment controller class. Holds all the methods that handle the applications requests._147 * This methods are mapped to a specific URL on the main Server file of the application._147 */_147@SuppressWarnings({"rawtypes", "unchecked"})_147public class AppointmentController {_147 private Scheduler scheduler;_147 private AppointmentService service;_147_147 public AppointmentController(AppointmentService service, Scheduler scheduler) {_147 this.service = service;_147 this.scheduler = scheduler;_147 }_147_147 public TemplateViewRoute renderCreatePage = (request, response) -> {_147 Map map = new HashMap();_147_147 map.put("zones", timeZones());_147 return new ModelAndView(map, "new.mustache");_147 };_147_147 public TemplateViewRoute index = (request, response) -> {_147 Map map = new HashMap();_147_147 List<Appointment> appointments = service.findAll();_147 map.put("appointments", appointments);_147_147 return new ModelAndView(map, "index.mustache");_147 };_147_147 public Route delete = (request, response) -> {_147 String id = request.queryParams("id");_147 Long idLong = Long.parseLong(id, 10);_147_147 Appointment appointment = service.getAppointment(idLong);_147 service.delete(appointment);_147_147 response.redirect("/");_147 return response;_147 };_147_147 /**_147 * Controller method that creates a new appointment. Also, schedules an_147 * appointment reminder once the actual appointment is persisted to the database._147 */_147 public TemplateViewRoute create = (request, response) -> {_147 FieldValidator validator =_147 new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});_147_147 if (validator.valid(request)) {_147 String name = request.queryParams("name");_147 String phoneNumber = request.queryParams("phoneNumber");_147 String date = request.queryParams("date");_147 int delta = 0;_147 try {_147 delta = Integer.parseInt(request.queryParams("delta"));_147 } catch (NumberFormatException e) {_147 System.out.println("Invalid format number for appointment delta");_147 }_147 String timeZone = request.queryParams("timeZone");_147_147 DateTimeZone zone = DateTimeZone.forID(timeZone);_147 DateTimeZone zoneUTC = DateTimeZone.UTC;_147_147 DateTime dt;_147 DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");_147 formatter = formatter.withZone(zone);_147 dt = formatter.parseDateTime(date);_147 formatter = formatter.withZone(zoneUTC);_147 String dateUTC = dt.toString(formatter);_147_147 Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);_147 service.create(appointment);_147_147 scheduleJob(appointment);_147_147 response.redirect("/");_147 }_147_147 Map map = new HashMap();_147_147 map.put("zones", timeZones());_147 return new ModelAndView(map, "new.mustache");_147 };_147_147 /**_147 * Schedules a AppointmentScheduler instance to be created and executed in the specified future_147 * date coming from the appointment entity_147 * @param appointment The newly created Appointment that has already been persisted to the DB._147 */_147 private void scheduleJob(Appointment appointment) {_147 String appointmentId = appointment.getId().toString();_147_147 DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());_147 DateTime dt;_147 DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");_147 formatter = formatter.withZone(zone);_147 dt = formatter.parseDateTime(appointment.getDate());_147 Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();_147_147 JobDetail job =_147 newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)_147 .usingJobData("appointmentId", appointmentId).build();_147_147 Trigger trigger =_147 newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();_147_147 try {_147 scheduler.scheduleJob(job, trigger);_147 } catch (SchedulerException e) {_147 System.out.println("Unable to schedule the Job");_147 }_147 }_147_147 private List<String> timeZones() {_147 TimeZones tz = new TimeZones();_147_147 return tz.getTimeZones();_147 }_147}
We will dig further into that function next.
The controller uses the injected scheduler to set up a notification. The AppointmentScheduler
class is used here to actually send out the notification via SMS through a Quartz trigger.
src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java
_147package com.twilio.appointmentreminders.controllers;_147_147import com.twilio.appointmentreminders.models.Appointment;_147import com.twilio.appointmentreminders.models.AppointmentService;_147import com.twilio.appointmentreminders.util.AppointmentScheduler;_147import com.twilio.appointmentreminders.util.FieldValidator;_147import com.twilio.appointmentreminders.util.TimeZones;_147import org.joda.time.DateTime;_147import org.joda.time.DateTimeZone;_147import org.joda.time.format.DateTimeFormat;_147import org.joda.time.format.DateTimeFormatter;_147import org.quartz.JobDetail;_147import org.quartz.Scheduler;_147import org.quartz.SchedulerException;_147import org.quartz.Trigger;_147import spark.ModelAndView;_147import spark.Route;_147import spark.TemplateViewRoute;_147_147import java.util.Date;_147import java.util.HashMap;_147import java.util.List;_147import java.util.Map;_147_147import static org.quartz.JobBuilder.newJob;_147import static org.quartz.TriggerBuilder.newTrigger;_147_147/**_147 * Appointment controller class. Holds all the methods that handle the applications requests._147 * This methods are mapped to a specific URL on the main Server file of the application._147 */_147@SuppressWarnings({"rawtypes", "unchecked"})_147public class AppointmentController {_147 private Scheduler scheduler;_147 private AppointmentService service;_147_147 public AppointmentController(AppointmentService service, Scheduler scheduler) {_147 this.service = service;_147 this.scheduler = scheduler;_147 }_147_147 public TemplateViewRoute renderCreatePage = (request, response) -> {_147 Map map = new HashMap();_147_147 map.put("zones", timeZones());_147 return new ModelAndView(map, "new.mustache");_147 };_147_147 public TemplateViewRoute index = (request, response) -> {_147 Map map = new HashMap();_147_147 List<Appointment> appointments = service.findAll();_147 map.put("appointments", appointments);_147_147 return new ModelAndView(map, "index.mustache");_147 };_147_147 public Route delete = (request, response) -> {_147 String id = request.queryParams("id");_147 Long idLong = Long.parseLong(id, 10);_147_147 Appointment appointment = service.getAppointment(idLong);_147 service.delete(appointment);_147_147 response.redirect("/");_147 return response;_147 };_147_147 /**_147 * Controller method that creates a new appointment. Also, schedules an_147 * appointment reminder once the actual appointment is persisted to the database._147 */_147 public TemplateViewRoute create = (request, response) -> {_147 FieldValidator validator =_147 new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});_147_147 if (validator.valid(request)) {_147 String name = request.queryParams("name");_147 String phoneNumber = request.queryParams("phoneNumber");_147 String date = request.queryParams("date");_147 int delta = 0;_147 try {_147 delta = Integer.parseInt(request.queryParams("delta"));_147 } catch (NumberFormatException e) {_147 System.out.println("Invalid format number for appointment delta");_147 }_147 String timeZone = request.queryParams("timeZone");_147_147 DateTimeZone zone = DateTimeZone.forID(timeZone);_147 DateTimeZone zoneUTC = DateTimeZone.UTC;_147_147 DateTime dt;_147 DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");_147 formatter = formatter.withZone(zone);_147 dt = formatter.parseDateTime(date);_147 formatter = formatter.withZone(zoneUTC);_147 String dateUTC = dt.toString(formatter);_147_147 Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);_147 service.create(appointment);_147_147 scheduleJob(appointment);_147_147 response.redirect("/");_147 }_147_147 Map map = new HashMap();_147_147 map.put("zones", timeZones());_147 return new ModelAndView(map, "new.mustache");_147 };_147_147 /**_147 * Schedules a AppointmentScheduler instance to be created and executed in the specified future_147 * date coming from the appointment entity_147 * @param appointment The newly created Appointment that has already been persisted to the DB._147 */_147 private void scheduleJob(Appointment appointment) {_147 String appointmentId = appointment.getId().toString();_147_147 DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());_147 DateTime dt;_147 DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");_147 formatter = formatter.withZone(zone);_147 dt = formatter.parseDateTime(appointment.getDate());_147 Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();_147_147 JobDetail job =_147 newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)_147 .usingJobData("appointmentId", appointmentId).build();_147_147 Trigger trigger =_147 newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();_147_147 try {_147 scheduler.scheduleJob(job, trigger);_147 } catch (SchedulerException e) {_147 System.out.println("Unable to schedule the Job");_147 }_147 }_147_147 private List<String> timeZones() {_147 TimeZones tz = new TimeZones();_147_147 return tz.getTimeZones();_147 }_147}
Let's look at how we handle this trigger.
Every time a scheduled job is triggered by Quartz, an instance of the AppointmentScheduler
class is created to handle the job. When the class is loaded, we create a RestClient
to interact with the Twilio API using our account credentials.
src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java
_63package com.twilio.appointmentreminders.util;_63_63import com.twilio.Twilio;_63import com.twilio.appointmentreminders.models.Appointment;_63import com.twilio.appointmentreminders.models.AppointmentService;_63import com.twilio.exception.TwilioException;_63import com.twilio.rest.api.v2010.account.Message;_63import com.twilio.type.PhoneNumber;_63import org.quartz.Job;_63import org.quartz.JobDataMap;_63import org.quartz.JobExecutionContext;_63import org.quartz.JobExecutionException;_63import org.slf4j.Logger;_63import org.slf4j.LoggerFactory;_63_63import javax.persistence.EntityManagerFactory;_63_63public class AppointmentScheduler implements Job {_63_63 private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);_63_63 private static AppSetup appSetup = new AppSetup();_63_63 public static final String ACCOUNT_SID = appSetup.getAccountSid();_63 public static final String AUTH_TOKEN = appSetup.getAuthToken();_63 public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();_63_63 public AppointmentScheduler() {}_63_63 public void execute(JobExecutionContext context) throws JobExecutionException {_63 AppSetup appSetup = new AppSetup();_63_63 EntityManagerFactory factory = appSetup.getEntityManagerFactory();_63 AppointmentService service = new AppointmentService(factory.createEntityManager());_63_63 // Initialize the Twilio client_63 Twilio.init(ACCOUNT_SID, AUTH_TOKEN);_63_63 JobDataMap dataMap = context.getJobDetail().getJobDataMap();_63_63 String appointmentId = dataMap.getString("appointmentId");_63_63 Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));_63 if (appointment != null) {_63 String name = appointment.getName();_63 String phoneNumber = appointment.getPhoneNumber();_63 String date = appointment.getDate();_63 String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";_63_63 try {_63 Message message = Message_63 .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)_63 .create();_63 System.out.println("Message sent! Message SID: " + message.getSid());_63 } catch(TwilioException e) {_63 logger.error("An exception occurred trying to send the message \"{}\" to {}." +_63 " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());_63 }_63_63_63 }_63 }_63}
Next let's look at how the SMS is sent.
When the execute
method is called on an AppointmentScheduler
instance, we use the Twilio REST API client to actually send a formatted reminder message to our customer via SMS.
src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java
_63package com.twilio.appointmentreminders.util;_63_63import com.twilio.Twilio;_63import com.twilio.appointmentreminders.models.Appointment;_63import com.twilio.appointmentreminders.models.AppointmentService;_63import com.twilio.exception.TwilioException;_63import com.twilio.rest.api.v2010.account.Message;_63import com.twilio.type.PhoneNumber;_63import org.quartz.Job;_63import org.quartz.JobDataMap;_63import org.quartz.JobExecutionContext;_63import org.quartz.JobExecutionException;_63import org.slf4j.Logger;_63import org.slf4j.LoggerFactory;_63_63import javax.persistence.EntityManagerFactory;_63_63public class AppointmentScheduler implements Job {_63_63 private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);_63_63 private static AppSetup appSetup = new AppSetup();_63_63 public static final String ACCOUNT_SID = appSetup.getAccountSid();_63 public static final String AUTH_TOKEN = appSetup.getAuthToken();_63 public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();_63_63 public AppointmentScheduler() {}_63_63 public void execute(JobExecutionContext context) throws JobExecutionException {_63 AppSetup appSetup = new AppSetup();_63_63 EntityManagerFactory factory = appSetup.getEntityManagerFactory();_63 AppointmentService service = new AppointmentService(factory.createEntityManager());_63_63 // Initialize the Twilio client_63 Twilio.init(ACCOUNT_SID, AUTH_TOKEN);_63_63 JobDataMap dataMap = context.getJobDetail().getJobDataMap();_63_63 String appointmentId = dataMap.getString("appointmentId");_63_63 Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));_63 if (appointment != null) {_63 String name = appointment.getName();_63 String phoneNumber = appointment.getPhoneNumber();_63 String date = appointment.getDate();_63 String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";_63_63 try {_63 Message message = Message_63 .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)_63 .create();_63 System.out.println("Message sent! Message SID: " + message.getSid());_63 } catch(TwilioException e) {_63 logger.error("An exception occurred trying to send the message \"{}\" to {}." +_63 " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());_63 }_63_63_63 }_63 }_63}
That's it! We've successfully set up automated appointment reminders for our customers, which will be delivered via SMS.
If you haven't already, be sure to check out the JavaDoc for the Twilio helper library and our guides for SMS and voice.
Did this help?
Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter... we'd love to hear your thoughts, and know what you're building!