How to build a scheduling application using Java and JavaSpark

How to build a scheduling application using Java and JavaSpark

15 min read

Let’s imagine that we own a gym equipment rental business. People can reserve our gym equipment for an hour, and we need a scheduling system that can handle these reservations. Using Nylas and the Nylas Java SDK, we can create a Java and JavaSpark scheduling system where people can log in using their Google account, select a time slot, and receive a confirmation email with an event attached.

With the Nylas Calendar API, we can easily support creating new calendars and events. Here’s how:

Is your system ready?

If you already have the Nylas Java SDK installed and your Java environment is configured, then continue reading this blog.

Otherwise, I would recommend that you read the post How to Send Emails with the Nylas Java SDK to understand the setup basics.

What are we going to talk about?

What our application will look like

When we launch our webpage, we need to provide a way for people to log into their Google accounts:

Sign up into the Java and JavaSpark scheduling app

When pressing the button, they will be redirected to this login screen:

Log using Gmail or another provider

If you want to use a different provider you will need to configure an app for each of them:

Choose your account

We choose the account we want to use and enter our password:

Application not verified

We need to continue, as our application is still in the testing phase:

Give access and permissions for our Java and Spark scheduling application

We’re going to need to access the calendar, so we need to click continue, although we can confirm that we’re just being asked for four services related to the calendar and nothing else.

Once we are logged in, we will be greeted and presented with the available spots for that particular day:

Our scheduling application running on Java and JavaSpark

A new event will be scheduled once we click on one:

Selecting an available spot

We will receive a confirmation email as well as an event on our calendar:

Reservation made

Now, we can see that the time slot that we selected is no longer available:

Available spots left

We can only reserve one spot per day, so attempting to reserve another will result in an error message:

No more available spots

Create a custom calendar

While we can use our default calendar, it would be better to have one specially made for this project. Instead of creating a script, let’s use the Nylas CLI.

Read how to do it on the blog post A Small Guide to Virtual Calendars.

Creating a Nylas App and a Google Cloud Platform Project

If you haven’t created a Nylas app yet, follow these steps:

Create a new app for our scheduling application

The application has been successfully created:

Dashboard overview

We need to press on Connect an email account:

Dashboard accounts

Enter your email address and press Connect:

Connect an account

You’re going to get this:

Error, but it's Ok

But no worries, it’s not your fault or ours. It’s Google’s increased security. Simply, let’s move to the next step.

Creating a Google Project

To save you some time and to make this post shorter, we already have all the instructions you need to follow in this blog post How to Setup Your First Google API Project

Creating the scheduling project

As we want to create a Java web application, our best option is to use Spark, one of the most popular Micro Frameworks in the Java world.

Our project is going to be called GymEquipment, and it will have a main class called GymSchedule.java. We will need to change the default pom.xml file and create three Handlebars templates called welcome.hbs, main.hbs and scheduled.hbs. This files should live inside a templates folders inside the resources folder.

We’re going to include some useful libraries:

  • spark-core → The Spark Web Framework
  • nylas-java-sdk → The Nylas Java SDK
  • dotenv-java → To enable using .env files
  • spark-template-handlebars → To allow handlebars on Spark projects

This is going to be our new pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>GymEquipment_Updated</groupId>
    <artifactId>GymEquipment_Updated</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-core</artifactId>
            <version>2.9.4</version>
        </dependency>
        <dependency>
            <groupId>com.nylas.sdk</groupId>
            <artifactId>nylas-java-sdk</artifactId>
            <version>1.19.2</version>
        </dependency>
        <dependency>
            <groupId>io.github.cdimascio</groupId>
            <artifactId>dotenv-java</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-template-handlebars</artifactId>
            <version>2.7.1</version>
        </dependency>
        <dependency>
            <groupId>com.googlecode.json-simple</groupId>
            <artifactId>json-simple</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <!-- Build an executable JAR -->
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.1.0</version>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>GymSchedule</mainClass>
                            </manifest>
                        </archive>
                    </configuration>
                </plugin>
                <plugin>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <configuration>
                        <archive>
                            <manifest>
                                <mainClass>GymSchedule</mainClass>
                            </manifest>
                        </archive>
                        <descriptorRefs>
                            <descriptorRef>jar-with-dependencies</descriptorRef>
                        </descriptorRefs>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>1.2.1</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>java</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <mainClass>GymSchedule</mainClass>
                        <cleanupDaemonThreads>false</cleanupDaemonThreads>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

And this is going to be our GymSchedule.java file:

// Import Java Utilities
import java.io.FileWriter;
import java.nio.file.Paths;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.io.File;

// Import Spark and Mustache libraries
import spark.ModelAndView;
import static spark.Spark.*;
import spark.template.handlebars.HandlebarsTemplateEngine;

//Import Nylas Packages
import com.nylas.*;

//Import DotEnv to handle .env files
import io.github.cdimascio.dotenv.Dotenv;

public class GymSchedule {
    public static void main(String[] args) {
        staticFiles.location("/public");
        // Load the .env file
        Dotenv dotenv = Dotenv.load();
        // Create the client object
        NylasClient client = new NylasClient();
        // Connect it to Nylas using the Access Token from the .env file
        NylasAccount account = client.account(dotenv.get("ACCESS_TOKEN"));
        // Connect to Nylas using the virtual calendar token
        NylasClient virtual_client = new NylasClient();
        NylasAccount virtual = virtual_client.account(dotenv.get("VIRTUAL_TOKEN"));
        // Create a user client object
        NylasClient api = new NylasClient();
        // Connect it to Nylas using the Client Id and Client Secret from the .env file
        NylasApplication application = api.application(dotenv.get("CLIENT_ID"), dotenv.get("CLIENT_SECRET"));
        // Create a user account based on the access_token
        AtomicReference<AccountDetail> api_account = new AtomicReference<>(new AccountDetail());

        // Hashmap to send parameters to our handlebars view
        Map map = new HashMap();

        // Default path when we load our web application
        get("/", (request, response) ->
                        new ModelAndView(map, "welcome.hbs"),
                new HandlebarsTemplateEngine());

        // Call the authorization page
        get("/login/nylas/authorized", (request, response) -> {
            if(request.session().attribute("email_address") == null){
                NativeAuthentication authentication = application.nativeAuthentication();
                String accessToken = authentication.fetchToken(request.queryParams("code")).getAccessToken();
                api_account.set(api.account(accessToken).fetchAccountByAccessToken());
                request.session().attribute("email_address", api_account.get().getEmailAddress());
                request.session().attribute("participant", api_account.get().getName());
                request.session().attribute("access_token", accessToken);
                response.redirect("/login");
            }
            return null;
        });

        get("/login", (request, response) -> {
            if(request.session().attribute("email_address") == null){
                HostedAuthentication authentication = application.hostedAuthentication();
                String hostedAuthUrl = authentication.urlBuilder()
                        .redirectUri("http://localhost:4567/login/nylas/authorized")
                        .responseType("code") // Use token for client-side apps
                        .scopes(Scope.CALENDAR)
                        .loginHint("[email protected]")
                        .state("mycustomstate")
                        .buildUrl();
                response.redirect(hostedAuthUrl);
            }
            ArrayList<ArrayList<String>> schedules = new ArrayList<>();
            List<TimeSlot> timeslots = new ArrayList<>();
            TimeSlot time_slots = new TimeSlot();

            Calendars calendars = virtual.calendars();

            // Get today's date
            LocalDate today = LocalDate.now();
            Instant sixPmUtc = today.atTime(10, 0).toInstant(ZoneOffset.UTC);

            // Access the Events endpoint
            Events events = virtual.events();
            List<FreeBusy> list_busy = new ArrayList<>();
            FreeBusy free_busy = new FreeBusy();

            RemoteCollection<Event> events_list = events.list(new EventQuery()
                    .calendarId(dotenv.get("CALENDAR_ID")));
            for (Event event : events_list){
                Event.Timespan timespan = (Event.Timespan) event.getWhen();
                time_slots.setStartTime(timespan.getStartTime());
                time_slots.setEndTime(timespan.getEndTime());
                timeslots.add(time_slots);
            }

            free_busy.setEmail("nylas_gym_calendar");
            free_busy.setTimeSlots(timeslots);
            list_busy.add(free_busy);

            FreeBusyCalendars freeBusyCalendars = new FreeBusyCalendars(dotenv.get("VIRTUAL_ACCOUNT"), Collections.singletonList(dotenv.get("CALENDAR_ID")));

            SingleAvailabilityQuery query = new SingleAvailabilityQuery()
                    .durationMinutes(60)
                    .startTime(sixPmUtc)
                    .endTime(sixPmUtc.plus(5, ChronoUnit.HOURS))
                    .intervalMinutes(60)
                    .calendars(freeBusyCalendars)
                    //.freeBusy(objTimeSlots)
                    .freeBusy(list_busy)
                    .emails(Collections.singletonList("nylas_gym_calendar"));

            Availability availability = calendars.availability(query);
            List<TimeSlot> aval_time_slots = availability.getTimeSlots();
            LocalDateTime ldt = LocalDateTime.now();
            String start_time;
            String end_time;
            String minutes;
            String seconds;
            for(TimeSlot available : aval_time_slots){
                ArrayList<String> schedule = new ArrayList<>();
                ldt = LocalDateTime.ofInstant(available.getStartTime(), ZoneOffset.UTC);
                if(ldt.getMinute() < 10) { minutes = ldt.getMinute() + "0"; }
                else { minutes = String.valueOf(ldt.getMinute());};
                if(ldt.getSecond() < 10) { seconds = ldt.getSecond() + "0"; }
                else { seconds = String.valueOf(ldt.getSecond());};
                start_time = ldt.getHour() + ":" + minutes + ":" + seconds;
                ldt = LocalDateTime.ofInstant(available.getEndTime(), ZoneOffset.UTC);
                if(ldt.getMinute() < 10) { minutes = ldt.getMinute() + "0"; }
                else { minutes = String.valueOf(ldt.getMinute());};
                if(ldt.getSecond() < 10) { seconds = ldt.getSecond() + "0"; }
                else { seconds = String.valueOf(ldt.getSecond());};
                end_time = ldt.getHour() + ":" + minutes + ":" + seconds;
                schedule.add(start_time);
                schedule.add(end_time);
                schedules.add(schedule);
            }
            map.clear();
            // Pass parameters for the view
            map.put("participant",request.session().attribute("participant"));
            map.put("schedules", schedules);
            // Call the handlebars template
            return new ModelAndView(map, "main.hbs");
        }, new HandlebarsTemplateEngine());

        // Create the event
        get("/schedule", (request, response) -> {
            String link = "";
            Boolean registered = false;

            // Check if participant has been already registered
            LocalDate today = LocalDate.now();
            Instant sixPmUtc = today.atTime(9, 0).toInstant(ZoneOffset.UTC);
            Events events = virtual.events();
            RemoteCollection<Event> events_list = events.list(new EventQuery()
                    .calendarId(dotenv.get("CALENDAR_ID"))
                    .startsAfter(sixPmUtc)
                    .endsBefore(sixPmUtc.plus(7,
                            ChronoUnit.HOURS )));
            for (Event event : events_list){
                for( Participant participant : event.getParticipants()){
                    if(participant.getEmail() == request.session().attribute("email_address")){
                        registered = true;
                    }
                }
            }

            if(!registered){
                // Get time parameters
                String ts = request.queryParams("start_time");
                String te = request.queryParams("end_time");
                link = ts + " to " + te;
                Instant event_time = today.atTime(Integer.parseInt(ts.substring(0,2)),Integer.parseInt(ts.substring(6,8))).toInstant(ZoneOffset.UTC);
                // We create the event details
                Event event = new Event(dotenv.get("CALENDAR_ID"),
                        new Event.Timespan(event_time.plus(5, ChronoUnit.HOURS), event_time.plus(6, ChronoUnit.HOURS)));
                event.setTitle("Nylas' Gym Equipment Reservation");
                event.setLocation("Nylas' Gym");
                event.setDescription("You have reserve Gym Equipment from " + ts + " to " +  te);
                event.setParticipants(
                        Arrays.asList(new Participant(request.session().attribute("email_address")).
                                name(request.session().attribute("participant")))
                );
                event.setBusy(true);
                // Create the event
                virtual.events().create(event, false);

                // Generate the Internet Calendar Scheduling
                String icsFromEvent = events.generateICS(event);
                //File myObj = new File("invite.ics");
                FileWriter myWriter = new FileWriter("invite.ics");
                myWriter.write(icsFromEvent);
                myWriter.close();
                Files files = account.files();
                byte[] myFile = java.nio.file.Files.readAllBytes(Paths.get("invite.ics"));
                com.nylas.File upload = files.upload("invite.ics", "application/txt", myFile);
                //Create a new draft
                Draft draft = new Draft();
                //Set the subject of the message
                draft.setSubject("Nylas\\' Gym Equipment Reservation");
                draft.setBody("You have reserve Gym Equipment from " + ts + " to " + te);
                draft.setTo(Arrays.asList(new NameEmail(request.session().attribute("participant"),
                        request.session().attribute("email_address"))));
                // Attach a file to a draft
                draft.attach(upload);
                try {
                    //Send the email draft
                    account.drafts().send(draft);
                } catch (Exception e) {
                    //Something went wrong, display the error
                    System.out.print(e.getMessage());
                }
            }

            // Call page to confirm reservation
            map.clear();
            // Pass parameters for the view
            map.put("link",link);
            map.put("registered", registered);
            return new ModelAndView(map, "scheduled.hbs");
        }, new HandlebarsTemplateEngine());

        // Remove auth access
        get("/remove", (request, response) -> {
            // Revoke the access token that was passed to the Nylas client.
            NylasAccount apiaccount = api.account(request.session().attribute("access_token"));
            apiaccount.revokeAccessToken();

            request.session().attribute("email_address", null);
            request.session().attribute("participant", null);
            request.session().attribute("access_token", null);
            response.redirect("/");
            return null;
        });
    }
}

On the templates folder, we’re going to start with main.hbs:

<html>
<head>
    <script src="https://cdn.tailwindcss.com"></script>
    <title>Nylas' Gym Equipment</title>
</head>
<body>
<div class="bg-[#315acb] border-green-600 border-b p-4 m-4 rounded grid place-items-center">
    <h1 class="text-4xl text-white">Nylas' Gym Equipment</h1>
    <br>
    <p class="text-3xl text-center text-white">Welcome {{participant}}</p><br>
    <p class="text-3xl text-center text-white">Choose a time that's convenient for you, so that we can reserve your gym equipment.</p><br><br>
    {{#each schedules}}
        <a href="schedule?start_time={{this.[0]}}&end_time={{this.[1]}}"><b>{{this.[0]}} to {{this.[1]}}</b></a><br>
    {{/each}}
    <a href="/remove" class="text-red-200"><b>Log Out</b></a>
</div>
</body>
</html>

Then the file welcome.hbs:

<html>
<head>
    <script src="https://cdn.tailwindcss.com"></script>
    <title>Nylas' Gym Equipment</title>
</head>
<body>
<div class="bg-[#315acb] border-green-600 border-b p-4 m-4 rounded grid place-items-center">
    <h1 class="text-4xl text-white">Welcome to Nylas' Gym Equipment</h1><br><br>
    <img src="images/Nyla_Space.png"><br><br><br>
    <a href="/login"><img src="images/Google.png" alt="Sign in"></a>
</div>
</body>
</html>

Now scheduled.html:

<html>
<head>
    <script src="https://cdn.tailwindcss.com"></script>
    <title>Nylas' Gym Equipment</title>
</head>
<body>
<div class="bg-[#315acb] border-green-600 border-b p-4 m-4 rounded grid place-items-center">
    {{#if registered }}
        <h1 class="text-4xl text-white">You have already scheduled the usage of Nylas' Gym Equipment. Only one time per customer.</h1><br>
        <h1 class="text-4xl text-white">If you want to cancel or update your reservation, contact <b>Customer Care</b>.</h1><br>
    {{ else }}
        <h1 class="text-4xl text-white">You have schedule the usage of Nylas' Gym Equipment</h1><br>
        <p class="text-3xl text-center text-white">You will receive a confirmation for your {{link}} reservation.</p><br>
    {{/if}}

    <p class="text-3xl text-center text-red">With love, Nylas.</p><br><br>
    <a href="/login" class="text-red-200"><b>Go back</b></a>
</div>
</div>
</body>
</html>

On the public.images folder, we need to images:

Sign with Google image
Nyla Portrait

Installing ngrok

Ngrok is a globally distributed reverse proxy that allows our local applications to be exposed on the internet. We need to create a user and then install the client.

We can install it by using brew:

$ brew install ngrok/ngrok/ngrok

Then, follow the instructions on the webpage.

The first option is the installation:

Configuring ngrok

Once everything is ready, we can run it by typing the following on the terminal window:

$ ./ngrok http 4567
Running ngrok

We need to copy the Forwarding address, which will change every time we run ngrok.

Running the scheduling application

To run our Java and JavaSpark Scheduling Application, we need to update our Nylas application. Head to the application settings page, choose Authentication and then type the following on the Add you callback input box:

http://localhost:4567/login/nylas/authorized

And also the address that you copied from ngrok:

https://xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx.ngrok.io
Configure Nylas and our Java and JavaSpark scheduling app interaction

we just need to type the following on the terminal window:

$ mvn package
$ mvn exec:java -Dexec.mainClass="GymEquipment" -Dexec.cleanupDaemonThreads=false
Running our JavaSpark Scheduling Application on Java

Our application will be running on port 4567 of localhost, so we just need to open our favourite browser and go to the following address:

http://localhost:4567

Everything should work as expected.

How to check for existing events?

Our application is done and working correctly, but, how do we check for existing events? Since the virtual calendar doesn’t exist on our calendar list, this might be tricky, but luckily it is not.

If you have the Nylas CLI installed (if you don’t, here’s how Working with the Nylas CLI) then just run this:

$ nylas api events get --access_token <VIRTUAL_ACCESS_TOKEN>

If you want to learn more about the Nylas Email API, please visit our documentation Nylas Calendar Documentation.

What’s Next?

Now that we have created a Java and JavaSpark Scheduling Application, why don’t we explore and create more.

You can sign up Nylas for free and start building!

Related resources

How to block time slots in Outlook and Google calendar with Nylas Calendar API

Key Takeaways Managing calendar availability is essential for professionals, teams, and businesses to stay organized…

How to Solve Webhook Integration Challenges with PubSub Notification Channel

Key Takeaways This article addresses the challenges of webhook integration and introduces the PubSub Notification…

How to Send Emails Using an API

Key Takeaways This post will provide a complete walkthrough for integrating an email API focused…