Appointment Reminders with Java and Spark

Download the Code

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.

Create the Quartz job scheduler

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.

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

import com.twilio.appointmentreminders.controllers.AppointmentController;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppSetup;
import com.twilio.appointmentreminders.util.LoggingFilter;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import spark.Spark;
import spark.template.mustache.MustacheTemplateEngine;

import javax.persistence.EntityManagerFactory;

import static spark.Spark.*;

/**
 * Main application class. The environment is set up here, and all necessary services are run.
 */
public class Server {
  public static void main(String[] args) {
    AppSetup appSetup = new AppSetup();

    /**
     * Sets the port in which the application will run. Takes the port value from PORT
     * environment variable, if not set, uses Spark default port 4567.
     */
    port(appSetup.getPortNumber());

    /**
     * Gets the entity manager based on environment variable DATABASE_URL and injects it into
     * AppointmentService which handles all DB operations.
     */
    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    /**
     * Specifies the directory within resources that will be publicly available when the
     * application is running. Place static web files in this directory (JS, CSS).
     */
    Spark.staticFileLocation("/public");

    /** Creates a new instance of Quartz Scheduler and starts it. */
    Scheduler scheduler = null;
    try {
      scheduler = StdSchedulerFactory.getDefaultScheduler();

      scheduler.start();

    } catch (SchedulerException se) {
      System.out.println("Unable to start scheduler service");
    }

    /** Injects AppointmentService and Scheduler into the controller. */
    AppointmentController controller = new AppointmentController(service, scheduler);

    /**
     * Defines all url paths for the application and assigns a controller method for each.
     * If the route renders a page, the templating engine must be specified, and the controller
     * should return the appropriate Route object.
     */
    get("/", controller.index, new MustacheTemplateEngine());
    get("/new", controller.renderCreatePage, new MustacheTemplateEngine());
    post("/create", controller.create, new MustacheTemplateEngine());
    post("/delete", controller.delete);

    afterAfter(new LoggingFilter());
  }
}
src/main/java/com/twilio/appointmentreminders/Server.java
Create the Quartz job scheduler

src/main/java/com/twilio/appointmentreminders/Server.java

Next let's see how we create a new Appointment.

Create an 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.

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

import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppointmentScheduler;
import com.twilio.appointmentreminders.util.FieldValidator;
import com.twilio.appointmentreminders.util.TimeZones;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import spark.ModelAndView;
import spark.Route;
import spark.TemplateViewRoute;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Appointment controller class. Holds all the methods that handle the applications requests.
 * This methods are mapped to a specific URL on the main Server file of the application.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class AppointmentController {
  private Scheduler scheduler;
  private AppointmentService service;

  public AppointmentController(AppointmentService service, Scheduler scheduler) {
    this.service = service;
    this.scheduler = scheduler;
  }

  public TemplateViewRoute renderCreatePage = (request, response) -> {
    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  public TemplateViewRoute index = (request, response) -> {
    Map map = new HashMap();

    List<Appointment> appointments = service.findAll();
    map.put("appointments", appointments);

    return new ModelAndView(map, "index.mustache");
  };

  public Route delete = (request, response) -> {
    String id = request.queryParams("id");
    Long idLong = Long.parseLong(id, 10);

    Appointment appointment = service.getAppointment(idLong);
    service.delete(appointment);

    response.redirect("/");
    return response;
  };

  /**
   * Controller method that creates a new appointment. Also, schedules an
   * appointment reminder once the actual appointment is persisted to the database.
   */
  public TemplateViewRoute create = (request, response) -> {
    FieldValidator validator =
        new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});

    if (validator.valid(request)) {
      String name = request.queryParams("name");
      String phoneNumber = request.queryParams("phoneNumber");
      String date = request.queryParams("date");
      int delta = 0;
      try {
        delta = Integer.parseInt(request.queryParams("delta"));
      } catch (NumberFormatException e) {
        System.out.println("Invalid format number for appointment delta");
      }
      String timeZone = request.queryParams("timeZone");

      DateTimeZone zone = DateTimeZone.forID(timeZone);
      DateTimeZone zoneUTC = DateTimeZone.UTC;

      DateTime dt;
      DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
      formatter = formatter.withZone(zone);
      dt = formatter.parseDateTime(date);
      formatter = formatter.withZone(zoneUTC);
      String dateUTC = dt.toString(formatter);

      Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
      service.create(appointment);

      scheduleJob(appointment);

      response.redirect("/");
    }

    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  /**
   * Schedules a AppointmentScheduler instance to be created and executed in the specified future
   * date coming from the appointment entity
   * @param appointment The newly created Appointment that has already been persisted to the DB.
   */
  private void scheduleJob(Appointment appointment) {
    String appointmentId = appointment.getId().toString();

    DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
    DateTime dt;
    DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
    formatter = formatter.withZone(zone);
    dt = formatter.parseDateTime(appointment.getDate());
    Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();

    JobDetail job =
        newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
            .usingJobData("appointmentId", appointmentId).build();

    Trigger trigger =
        newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();

    try {
      scheduler.scheduleJob(job, trigger);
    } catch (SchedulerException e) {
      System.out.println("Unable to schedule the Job");
    }
  }

  private List<String> timeZones() {
    TimeZones tz = new TimeZones();

    return tz.getTimeZones();
  }
}
src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java
Create an appointment

src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java

We will dig further into that function next.

Schedule the reminder job

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.

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

import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppointmentScheduler;
import com.twilio.appointmentreminders.util.FieldValidator;
import com.twilio.appointmentreminders.util.TimeZones;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import spark.ModelAndView;
import spark.Route;
import spark.TemplateViewRoute;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Appointment controller class. Holds all the methods that handle the applications requests.
 * This methods are mapped to a specific URL on the main Server file of the application.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class AppointmentController {
  private Scheduler scheduler;
  private AppointmentService service;

  public AppointmentController(AppointmentService service, Scheduler scheduler) {
    this.service = service;
    this.scheduler = scheduler;
  }

  public TemplateViewRoute renderCreatePage = (request, response) -> {
    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  public TemplateViewRoute index = (request, response) -> {
    Map map = new HashMap();

    List<Appointment> appointments = service.findAll();
    map.put("appointments", appointments);

    return new ModelAndView(map, "index.mustache");
  };

  public Route delete = (request, response) -> {
    String id = request.queryParams("id");
    Long idLong = Long.parseLong(id, 10);

    Appointment appointment = service.getAppointment(idLong);
    service.delete(appointment);

    response.redirect("/");
    return response;
  };

  /**
   * Controller method that creates a new appointment. Also, schedules an
   * appointment reminder once the actual appointment is persisted to the database.
   */
  public TemplateViewRoute create = (request, response) -> {
    FieldValidator validator =
        new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});

    if (validator.valid(request)) {
      String name = request.queryParams("name");
      String phoneNumber = request.queryParams("phoneNumber");
      String date = request.queryParams("date");
      int delta = 0;
      try {
        delta = Integer.parseInt(request.queryParams("delta"));
      } catch (NumberFormatException e) {
        System.out.println("Invalid format number for appointment delta");
      }
      String timeZone = request.queryParams("timeZone");

      DateTimeZone zone = DateTimeZone.forID(timeZone);
      DateTimeZone zoneUTC = DateTimeZone.UTC;

      DateTime dt;
      DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
      formatter = formatter.withZone(zone);
      dt = formatter.parseDateTime(date);
      formatter = formatter.withZone(zoneUTC);
      String dateUTC = dt.toString(formatter);

      Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
      service.create(appointment);

      scheduleJob(appointment);

      response.redirect("/");
    }

    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  /**
   * Schedules a AppointmentScheduler instance to be created and executed in the specified future
   * date coming from the appointment entity
   * @param appointment The newly created Appointment that has already been persisted to the DB.
   */
  private void scheduleJob(Appointment appointment) {
    String appointmentId = appointment.getId().toString();

    DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
    DateTime dt;
    DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
    formatter = formatter.withZone(zone);
    dt = formatter.parseDateTime(appointment.getDate());
    Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();

    JobDetail job =
        newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
            .usingJobData("appointmentId", appointmentId).build();

    Trigger trigger =
        newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();

    try {
      scheduler.scheduleJob(job, trigger);
    } catch (SchedulerException e) {
      System.out.println("Unable to schedule the Job");
    }
  }

  private List<String> timeZones() {
    TimeZones tz = new TimeZones();

    return tz.getTimeZones();
  }
}
src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java
Schedule the reminder job

src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java

Let's look at how we handle this trigger.

Configure the application to send SMS messages

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.

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

import com.twilio.Twilio;
import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.EntityManagerFactory;

public class AppointmentScheduler implements Job {

  private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);

  private static AppSetup appSetup = new AppSetup();

  public static final String ACCOUNT_SID = appSetup.getAccountSid();
  public static final String AUTH_TOKEN = appSetup.getAuthToken();
  public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();

  public AppointmentScheduler() {}

  public void execute(JobExecutionContext context) throws JobExecutionException {
    AppSetup appSetup = new AppSetup();

    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    // Initialize the Twilio client
    Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String appointmentId = dataMap.getString("appointmentId");

    Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
    if (appointment != null) {
      String name = appointment.getName();
      String phoneNumber = appointment.getPhoneNumber();
      String date = appointment.getDate();
      String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";

      try {
        Message message = Message
                .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
                .create();
        System.out.println("Message sent! Message SID: " + message.getSid());
      } catch(TwilioException e) {
        logger.error("An exception occurred trying to send the message \"{}\" to {}." +
                " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
      }


    }
  }
}
src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java
Configure the application to send SMS messages

src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java

Next let's look at how the SMS is sent.

Send an SMS message from a background job

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.

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

import com.twilio.Twilio;
import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.EntityManagerFactory;

public class AppointmentScheduler implements Job {

  private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);

  private static AppSetup appSetup = new AppSetup();

  public static final String ACCOUNT_SID = appSetup.getAccountSid();
  public static final String AUTH_TOKEN = appSetup.getAuthToken();
  public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();

  public AppointmentScheduler() {}

  public void execute(JobExecutionContext context) throws JobExecutionException {
    AppSetup appSetup = new AppSetup();

    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    // Initialize the Twilio client
    Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String appointmentId = dataMap.getString("appointmentId");

    Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
    if (appointment != null) {
      String name = appointment.getName();
      String phoneNumber = appointment.getPhoneNumber();
      String date = appointment.getDate();
      String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";

      try {
        Message message = Message
                .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
                .create();
        System.out.println("Message sent! Message SID: " + message.getSid());
      } catch(TwilioException e) {
        logger.error("An exception occurred trying to send the message \"{}\" to {}." +
                " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
      }


    }
  }
}
src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java
Scheduled task to send SMS messages

src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java

That's it! We've successfully set up automated appointment reminders for our customers, which will be delivered via SMS.

Where to next?

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!

Mario Celi
Kat King
David Prothero
Andrew Baker
Hector Ortega
Agustin Camino

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.appointmentreminders;

import com.twilio.appointmentreminders.controllers.AppointmentController;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppSetup;
import com.twilio.appointmentreminders.util.LoggingFilter;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import spark.Spark;
import spark.template.mustache.MustacheTemplateEngine;

import javax.persistence.EntityManagerFactory;

import static spark.Spark.*;

/**
 * Main application class. The environment is set up here, and all necessary services are run.
 */
public class Server {
  public static void main(String[] args) {
    AppSetup appSetup = new AppSetup();

    /**
     * Sets the port in which the application will run. Takes the port value from PORT
     * environment variable, if not set, uses Spark default port 4567.
     */
    port(appSetup.getPortNumber());

    /**
     * Gets the entity manager based on environment variable DATABASE_URL and injects it into
     * AppointmentService which handles all DB operations.
     */
    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    /**
     * Specifies the directory within resources that will be publicly available when the
     * application is running. Place static web files in this directory (JS, CSS).
     */
    Spark.staticFileLocation("/public");

    /** Creates a new instance of Quartz Scheduler and starts it. */
    Scheduler scheduler = null;
    try {
      scheduler = StdSchedulerFactory.getDefaultScheduler();

      scheduler.start();

    } catch (SchedulerException se) {
      System.out.println("Unable to start scheduler service");
    }

    /** Injects AppointmentService and Scheduler into the controller. */
    AppointmentController controller = new AppointmentController(service, scheduler);

    /**
     * Defines all url paths for the application and assigns a controller method for each.
     * If the route renders a page, the templating engine must be specified, and the controller
     * should return the appropriate Route object.
     */
    get("/", controller.index, new MustacheTemplateEngine());
    get("/new", controller.renderCreatePage, new MustacheTemplateEngine());
    post("/create", controller.create, new MustacheTemplateEngine());
    post("/delete", controller.delete);

    afterAfter(new LoggingFilter());
  }
}
package com.twilio.appointmentreminders.controllers;

import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppointmentScheduler;
import com.twilio.appointmentreminders.util.FieldValidator;
import com.twilio.appointmentreminders.util.TimeZones;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import spark.ModelAndView;
import spark.Route;
import spark.TemplateViewRoute;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Appointment controller class. Holds all the methods that handle the applications requests.
 * This methods are mapped to a specific URL on the main Server file of the application.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class AppointmentController {
  private Scheduler scheduler;
  private AppointmentService service;

  public AppointmentController(AppointmentService service, Scheduler scheduler) {
    this.service = service;
    this.scheduler = scheduler;
  }

  public TemplateViewRoute renderCreatePage = (request, response) -> {
    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  public TemplateViewRoute index = (request, response) -> {
    Map map = new HashMap();

    List<Appointment> appointments = service.findAll();
    map.put("appointments", appointments);

    return new ModelAndView(map, "index.mustache");
  };

  public Route delete = (request, response) -> {
    String id = request.queryParams("id");
    Long idLong = Long.parseLong(id, 10);

    Appointment appointment = service.getAppointment(idLong);
    service.delete(appointment);

    response.redirect("/");
    return response;
  };

  /**
   * Controller method that creates a new appointment. Also, schedules an
   * appointment reminder once the actual appointment is persisted to the database.
   */
  public TemplateViewRoute create = (request, response) -> {
    FieldValidator validator =
        new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});

    if (validator.valid(request)) {
      String name = request.queryParams("name");
      String phoneNumber = request.queryParams("phoneNumber");
      String date = request.queryParams("date");
      int delta = 0;
      try {
        delta = Integer.parseInt(request.queryParams("delta"));
      } catch (NumberFormatException e) {
        System.out.println("Invalid format number for appointment delta");
      }
      String timeZone = request.queryParams("timeZone");

      DateTimeZone zone = DateTimeZone.forID(timeZone);
      DateTimeZone zoneUTC = DateTimeZone.UTC;

      DateTime dt;
      DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
      formatter = formatter.withZone(zone);
      dt = formatter.parseDateTime(date);
      formatter = formatter.withZone(zoneUTC);
      String dateUTC = dt.toString(formatter);

      Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
      service.create(appointment);

      scheduleJob(appointment);

      response.redirect("/");
    }

    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  /**
   * Schedules a AppointmentScheduler instance to be created and executed in the specified future
   * date coming from the appointment entity
   * @param appointment The newly created Appointment that has already been persisted to the DB.
   */
  private void scheduleJob(Appointment appointment) {
    String appointmentId = appointment.getId().toString();

    DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
    DateTime dt;
    DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
    formatter = formatter.withZone(zone);
    dt = formatter.parseDateTime(appointment.getDate());
    Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();

    JobDetail job =
        newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
            .usingJobData("appointmentId", appointmentId).build();

    Trigger trigger =
        newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();

    try {
      scheduler.scheduleJob(job, trigger);
    } catch (SchedulerException e) {
      System.out.println("Unable to schedule the Job");
    }
  }

  private List<String> timeZones() {
    TimeZones tz = new TimeZones();

    return tz.getTimeZones();
  }
}
package com.twilio.appointmentreminders.controllers;

import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppointmentScheduler;
import com.twilio.appointmentreminders.util.FieldValidator;
import com.twilio.appointmentreminders.util.TimeZones;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import spark.ModelAndView;
import spark.Route;
import spark.TemplateViewRoute;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Appointment controller class. Holds all the methods that handle the applications requests.
 * This methods are mapped to a specific URL on the main Server file of the application.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class AppointmentController {
  private Scheduler scheduler;
  private AppointmentService service;

  public AppointmentController(AppointmentService service, Scheduler scheduler) {
    this.service = service;
    this.scheduler = scheduler;
  }

  public TemplateViewRoute renderCreatePage = (request, response) -> {
    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  public TemplateViewRoute index = (request, response) -> {
    Map map = new HashMap();

    List<Appointment> appointments = service.findAll();
    map.put("appointments", appointments);

    return new ModelAndView(map, "index.mustache");
  };

  public Route delete = (request, response) -> {
    String id = request.queryParams("id");
    Long idLong = Long.parseLong(id, 10);

    Appointment appointment = service.getAppointment(idLong);
    service.delete(appointment);

    response.redirect("/");
    return response;
  };

  /**
   * Controller method that creates a new appointment. Also, schedules an
   * appointment reminder once the actual appointment is persisted to the database.
   */
  public TemplateViewRoute create = (request, response) -> {
    FieldValidator validator =
        new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});

    if (validator.valid(request)) {
      String name = request.queryParams("name");
      String phoneNumber = request.queryParams("phoneNumber");
      String date = request.queryParams("date");
      int delta = 0;
      try {
        delta = Integer.parseInt(request.queryParams("delta"));
      } catch (NumberFormatException e) {
        System.out.println("Invalid format number for appointment delta");
      }
      String timeZone = request.queryParams("timeZone");

      DateTimeZone zone = DateTimeZone.forID(timeZone);
      DateTimeZone zoneUTC = DateTimeZone.UTC;

      DateTime dt;
      DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
      formatter = formatter.withZone(zone);
      dt = formatter.parseDateTime(date);
      formatter = formatter.withZone(zoneUTC);
      String dateUTC = dt.toString(formatter);

      Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
      service.create(appointment);

      scheduleJob(appointment);

      response.redirect("/");
    }

    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  /**
   * Schedules a AppointmentScheduler instance to be created and executed in the specified future
   * date coming from the appointment entity
   * @param appointment The newly created Appointment that has already been persisted to the DB.
   */
  private void scheduleJob(Appointment appointment) {
    String appointmentId = appointment.getId().toString();

    DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
    DateTime dt;
    DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
    formatter = formatter.withZone(zone);
    dt = formatter.parseDateTime(appointment.getDate());
    Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();

    JobDetail job =
        newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
            .usingJobData("appointmentId", appointmentId).build();

    Trigger trigger =
        newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();

    try {
      scheduler.scheduleJob(job, trigger);
    } catch (SchedulerException e) {
      System.out.println("Unable to schedule the Job");
    }
  }

  private List<String> timeZones() {
    TimeZones tz = new TimeZones();

    return tz.getTimeZones();
  }
}
package com.twilio.appointmentreminders.util;

import com.twilio.Twilio;
import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.EntityManagerFactory;

public class AppointmentScheduler implements Job {

  private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);

  private static AppSetup appSetup = new AppSetup();

  public static final String ACCOUNT_SID = appSetup.getAccountSid();
  public static final String AUTH_TOKEN = appSetup.getAuthToken();
  public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();

  public AppointmentScheduler() {}

  public void execute(JobExecutionContext context) throws JobExecutionException {
    AppSetup appSetup = new AppSetup();

    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    // Initialize the Twilio client
    Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String appointmentId = dataMap.getString("appointmentId");

    Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
    if (appointment != null) {
      String name = appointment.getName();
      String phoneNumber = appointment.getPhoneNumber();
      String date = appointment.getDate();
      String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";

      try {
        Message message = Message
                .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
                .create();
        System.out.println("Message sent! Message SID: " + message.getSid());
      } catch(TwilioException e) {
        logger.error("An exception occurred trying to send the message \"{}\" to {}." +
                " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
      }


    }
  }
}
package com.twilio.appointmentreminders.util;

import com.twilio.Twilio;
import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.EntityManagerFactory;

public class AppointmentScheduler implements Job {

  private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);

  private static AppSetup appSetup = new AppSetup();

  public static final String ACCOUNT_SID = appSetup.getAccountSid();
  public static final String AUTH_TOKEN = appSetup.getAuthToken();
  public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();

  public AppointmentScheduler() {}

  public void execute(JobExecutionContext context) throws JobExecutionException {
    AppSetup appSetup = new AppSetup();

    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    // Initialize the Twilio client
    Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String appointmentId = dataMap.getString("appointmentId");

    Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
    if (appointment != null) {
      String name = appointment.getName();
      String phoneNumber = appointment.getPhoneNumber();
      String date = appointment.getDate();
      String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";

      try {
        Message message = Message
                .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
                .create();
        System.out.println("Message sent! Message SID: " + message.getSid());
      } catch(TwilioException e) {
        logger.error("An exception occurred trying to send the message \"{}\" to {}." +
                " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
      }


    }
  }
}