Example Project

The Reservation Manager application

In these lecture notes I am going to take you through the construction of our first full-featured JavaFX application. The application is an interactive scheduling program that schedules activities in conference rooms at a hotel.

Here is what the interface for the completed Reservation Manager program will look like:

The application's interface consists of three sections. At the very top of the window there is a menu bar with File and Reservation menus. Below the menu bar we see a DatePicker control that allows the user to select a date and a ChoiceBox control that allows the user to switch between available conference rooms at the hotel. The bottom section is a ListView control that displays all of the currently scheduled events for the given room and date. The events are displayed in a somewhat cryptic notation that shows the reservation number, group size, start time and end time for each event.

Application data

Here now is some additional detail about the problem this application is meant to solve.

A local hotel has hired you to write a program to manage reservations for their conference rooms. The hotel has a set of conference rooms with the following names and capacities:

Room NameCapacity
Grand ballroom50
Oneida room30
Michigan room20
Mendota room20
Sunrise room12

Information about room reservations for these conference rooms is stored in a text file. Each line of the text file contains information about a single reservation in this format:

<reservation number> <customer> <group size> <day> <start time> <duration>

The start times of events are integer hours in 24 hour format. For example, an event starting at 3 PM would be listed as starting at hour 15. Here is a more detailed description of each of these line items.

ItemDescription
reservation numberUnique identifying number assigned to a reservation - integer
customerUnique identifying number assigned to a customer - integer
group sizeNumber of people in the group - integer
dayDay of the reservation
start timeStart hour of the event in 24 hour format - integer
durationDuration of the event in hours - integer

When the program starts up it will read in a list of existing reservations from the text file. The application will then allow the user to view these reservations by date and room and make new reservations. The application will also allow the user to save the updated list of reservations back to the text file.

Model classes

Since the application we are going to build has to track a significant amount of data, we will need to start by creating an appropriate set of classes to record this data. We are going to need three data classes: a Hotel class that represents a collection of meeting rooms, a Room class that represents a meeting room, and an Reservation class that represents a reservation for an event scheduled in a meeting room.

The Reservation class represents an event to be scheduled in one of the rooms at the hotel. This class has a fairly obvious structure:

package edu.lawrence.hotel;

import java.io.PrintWriter;
import java.time.LocalDate;
import java.util.Scanner;

public class Reservation implements Comparable<Reservation> {
    private int eventNumber;
    private int customerNumber;
    private LocalDate day;
    private int startHour;
    private int endHour;
    private int groupSize;

    public Reservation() {

    }

    public Reservation(int eventNumber,int customerNumber,
        LocalDate day,int startHour,int endHour,int groupSize) {
        this.eventNumber = eventNumber;
        this.customerNumber = customerNumber;
        this.day = day;
        this.startHour = startHour;
        this.endHour = endHour;
        this.groupSize = groupSize;
    }

    public int getGroupSize() { return groupSize; }
    public LocalDate getDate() { return day; }

    public void readFrom(Scanner input) {
        eventNumber = input.nextInt();
        customerNumber = input.nextInt();
        input.useDelimiter(" |/");
        int year = input.nextInt();
        int month = input.nextInt();
        int dayOfMonth = input.nextInt();
        input.reset();
        day = LocalDate.of(year, month, dayOfMonth);
        startHour = input.nextInt();
        endHour = input.nextInt();
        groupSize = input.nextInt();
    }

    public void writeTo(PrintWriter output) {
        output.print(eventNumber);
        output.print(" ");
        output.print(customerNumber);
        output.print(" ");
        output.print(day.getYear());
        output.print("/");
        output.print(day.getMonthValue());
        output.print("/");
        output.print(day.getDayOfMonth());
        output.print(" ");
        output.print(startHour);
        output.print(" ");
        output.print(endHour);
        output.print(" ");
        output.println(groupSize);
    }

    public String toString() {
        return eventNumber + "(" + groupSize + "):" +
               startHour + "-" + endHour;
    }

    @Override
    public int compareTo(Reservation o) {
        if(day.isBefore(o.day))
            return -1;
        if(day.isAfter(o.day))
            return 1;
        if(endHour <= o.startHour)
            return -1;
        else if(startHour >= o.endHour)
            return 1;
        return 0;
    }
}

Several things are worth noting here. Each Reservation has a day member variable to represent the day on which the event is scheduled. I used the java.time.LocalDate data type for this purpose to play well with the JavaFX DatePicker component, which uses the same data type to represent days the user seleects. The second thing to note is that since we are going to be storing Reservations in a text file we will need a pair of methods to read and write the Reservations from that text file. Third, since we are going to want to display Reservations in the ListView in the GUI, the Reservation class needs a toString method that the ListView can use to construct a String to display the Reservation. Finally, since we will want our Reservations to be displayed in order of increasing start times we will need a compareTo method to facilitate eventual sorting of our Reservations. Note that compareTo has a slightly unusual structure: normally compareTo is supposed to return 0 only when the two Reservations being compared are exactly the same. Instead, this version of compareTo returns 0 if the two Reservations overlap in any way. This is intentional, and I will eventually explain why this is helpful.

Reservation objects are assigned to individual rooms. Here is the code for the Room class:

package edu.lawrence.hotel;

import java.io.PrintWriter;
import java.util.Scanner;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class Room {
    private String name;
    private int capacity;
    private ObservableList<Reservation> reservations;

    public Room() {
        reservations = FXCollections.observableArrayList();
    }

    public String getName() { return name; }

    public boolean allowsReservation(Reservation newReservation) {
        if(newReservation.getGroupSize() > capacity)
            return false;
        for(Reservation e : reservations) {
            if(e.compareTo(newReservation) == 0)
                return false;
        }
        return true;
    }

    public void addReservation(Reservation newReservation) {
        reservations.add(newReservation);
        FXCollections.sort(reservations);
    }

    public void removeReservation(Reservation toRemove) {
        reservations.remove(toRemove);
    }

    public void readFrom(Scanner input) {
        // Read the room details and reservations from the file
        name = input.next() + input.nextLine();
        capacity = input.nextInt();
        int howMany = input.nextInt();
        for(int n = 0;n < howMany;n++) {
            Reservation nextReservation = new Reservation();
            nextReservation.readFrom(input);
            reservations.add(nextReservation);
        }
        FXCollections.sort(reservations);
    }

    public void writeTo(PrintWriter output) {
        output.println(name);
        output.println(capacity);
        int howMany = reservations.size();
        output.println(howMany);
        for(int n = 0;n < howMany;n++)
            reservations.get(n).writeTo(output);
    }

    public ObservableList<Reservation> getReservations() {
        return reservations; }
}

Every Room has a name and a capacity, along with a list of Reservations scheduled into that Room. Normally, we would use an ArrayList<Reservation> to store those Reservation objects. However, since we are about to hook this Room class into a JavaFX GUI that will present these Reservations, we are going to have to use a different arrangement. The ListView class that we are going to display these events in will require us to provide a JavaFX observable list containing the Reservations. An observable list in JavaFX is a list that both provides some data but also provides hooks for change listeners that the ListView can hook into. When we change the contents of an observable list, the list will fire a change event that the ListView will pick up and use to redraw its contents.

The statement

reservations = FXCollections.observableArrayList();

in the Room class's constructor uses a static method to build an observable list that is backed by an ArrayList. This is all we will need to get started working with the observable list.

Because we will always want to maintain our Reservations in sorted order by time, we will need to sort the list after reading is done and whenever we add a new Reservation. This is done by a simple call to

FXCollections.sort(reservations);

which is the equivalent to Collections.sort for property lists. (Recall that we have already equipped our Reservation class with a compareTo method, so the sort method can do its job.)

Finally, note the allowsReservation method, which determines whether or not a prospective new Reservation can be scheduled into this Room. Since we earlier set up compareTo in the Reservation class to return 0 in case of an overlap, all we have to do here is to search the existing events to see if any of them overlap in time with the new Reservation. If any of them do, we can stop and immediately return false.

The final data class we are going to need is the Hotel class, which acts as a container for the various Rooms:

package edu.lawrence.hotel;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Scanner;

public class Hotel {
    private ArrayList<Room> rooms;

    public Hotel() {
        rooms = new ArrayList<Room>();
    }

    public void readFrom(Scanner input) {
        // Read the five rooms from the input file
        for(int n = 0;n < 5;n++) {
            Room newRoom = new Room();
            newRoom.readFrom(input);
            rooms.add(newRoom);
        }
    }

    public void writeTo(PrintWriter output) {
        for(Room r : rooms) {
            r.writeTo(output);
        }
    }

    public Room getRoom(String forName) {
        for(Room r : rooms) {
            if(r.getName().equalsIgnoreCase(forName))
                return r;
        }
        return null;
    }
}

The only thing worth commenting on here is the last method, which allows us to search for a Room by its name. We will need this because the GUI contains a ChoiceBox that lists the names of the rooms. When the user makes a selection from that ChoiceBox, we will read the room name the user selected and use that to search for the corresponding Room object in the Hotel class.

The controller class for the main window

The controller class for the main application window is the FXMLDocumentController class. This class contains the following member variables:

private Hotel model;
private Room currentRoom;
private LocalDate selectedDate;

@FXML
private ListView reservationsList;

@FXML
private DatePicker datePicker;

@FXML
private ChoiceBox roomChoice;

The last three member variables provide access the interface components that will display the reservations, the date and the list of rooms.

The model member variable gives us access the Hotel object, which in turn will give us access to Rooms and Reservations. I gave this member variable the name model because the main class that provides access to data in a JavaFX application is commonly referred to as the model class.

When the user selects a room name from the ChoiceBox, we will look up the corresponding Room object in the Hotel and store it in the currentRoom member variable. Likewise, when the user uses the DatePicker to select a day in time, we will store the selected day in the selectedDate member variable.

Getting things set up

Every controller class is expected to provide an initialize method to set up the interface. Here is the initialize method for this controller:

@Override
public void initialize(URL url, ResourceBundle rb) {
  selectedDate = LocalDate.now();
  datePicker.setValue(selectedDate);
    roomChoice.getItems().addAll("Grand Ballroom",
     "Oneida","Michigan","Mendota","Sunrise");
  roomChoice.setValue("Grand Ballroom");
  roomChoice.getSelectionModel().selectedItemProperty().
addListener((observable, oldValue, newValue)->setRoom(newValue));

  model = new Hotel();
  Scanner input = null;
  try {
    input = new Scanner(new File("reservations.txt"));
    model.readFrom(input);
    input.close();
    currentRoom = model.getRoom("Grand Ballroom");
    } catch (Exception ex) {
    System.out.println("Can not load data file.");
    ex.printStackTrace();
    if(input != null)
      input.close();
   }

  FilteredList<Reservation> filteredReservations =
      new FilteredList<Reservation>(
          currentRoom.getReservations(),
          (event)->event.getDate().equals(selectedDate));
  reservationsList.setItems(filteredReservations);
}

The top portion of this method sets up the DatePicker and the ChoiceBox, initializing the DatePicker with today's date and the ChoiceBox with a fixed list of room names. To handle the selection event that gets fired when the user makes a room choice from the ChoiceBox, we set up a listener for that event with the ChoiceBox's selection model. That model provides a selected item property that we can hook a change listener to using a lambda expression. That lambda expression gets the name of the newly selected room (newValue) and passes that room name to the setRoom() method in the controller.

The middle portion of the initialize() method creates the Hotel object and reads its contents from a text file.

After reading the Reservations in, we will need to fetch one of the Rooms from the Hotel and display its reservations in reservationsList. At this point we have to make an important adjustment. Recall that Rooms will contain Reservations for many different days: we just want to display the Reservations for the currently selected date. To solve this problem, JavaFX allows us to wrap a FilteredList object around the property containing the Reservations for that Room. The constructor for the FilteredList takes a second parameter which is a lambda expression that will get applied to each Reservation in the property list. Only the Reservations for which the lambda expression returns true will get displayed in the ListView. By hooking the filtered property list into the ListView with

reservationsList.setItems(filteredReservations);

we will have the list view display only events for the currently selected day. If we had wanted to display all of the Events in the Room without filtering, we would have instead done

reservationsList.setItems(currentRoom.getReservations());

Setting up the menus

The user interface for the main window contains a VBox with two elements: a menu bar and a BorderPane that contains the rest of the interface. A BorderPane is a type of container that contains a large central area along with smaller border regions on all four sides of the central area. In this application the central area is occupied by the list of reservations, while the top border area contains an HBox the holds the DatePicker and the ChoiceBox.

A menu bar is essentially a container for one or more Menu objects. In turn, each Menu object is a container for one or more MenuItem objects. MenuItems are very similar to Buttons in that they display text and can have an associated action method. You associate an action method with a MenuItem in exactly the same way you would associated an action method with a Button.

The File menu contains Save and Quit commands to save the application's data. The Reservations menu contains commands to add a new reservation and to remove a selected reservation.

Making new reservations

The Add Reservation... MenuItem in the Reservations menu is used to add Reservation objects to the currently selected room. When the user selects this menu item they will see a dialog box pop up that prompts them to enter details on the new event.

Implementing this dialog box requires the following steps:

  1. We have to add a new FXML file to the project that we can use to set up the interface for the dialog, along with a controller class for the dialog.
  2. In the event handler code for the Add Reservation... button on the main window we need to set up code that makes a new Stage for the dialog, loads the contents of the dialog from the FXML file, and then makes the dialog visible.
  3. The action event handler code for the Create button in the dialog has to have some way of creating the new Reservation object and adding it to the appropriate room.

To make a new FXML file/controller class combination we simply right-click on the folder inside Other Sources that contains JavaFX resource files in NetBeans and select the option to create a new Empty FXML file. When then also have to create a controller class in the appropriate source package.

Here is the code I wrote for the dialog's controller class.

public class FXMLNewReservationDialogController implements Initializable {
    private Room room;
    private LocalDate date;

    @FXML
    private TextField reservationNumber;
    @FXML
    private TextField customerNumber;
    @FXML
    private TextField groupSize;
    @FXML
    private TextField startTime;
    @FXML
    private TextField endTime;
    @FXML
    private void cancelDialog(ActionEvent event) {
        reservationNumber.getScene().getWindow().hide();
    }
    @FXML
    private Label errorText;

    @FXML
    private void acceptDialog(ActionEvent event) {
        int rsvNumber = Integer.parseInt(reservationNumber.getText());
        int custNumber = Integer.parseInt(customerNumber.getText());
        int start = Integer.parseInt(startTime.getText());
        int end = Integer.parseInt(endTime.getText());
        int size = Integer.parseInt(groupSize.getText());
        Reservation newReservation = new Reservation(rsvNumber,custNumber,date,start,end,size);
        if(room.allowsReservation(newReservation)) {
            room.addReservation(newReservation);
            reservationNumber.getScene().getWindow().hide();
        } else
            errorText.setText("The reservation can not be scheduled.");
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }

    public void setRoom(Room room) {
        this.room = room;
    }

    public void setDate(LocalDate date) {
        this.date = date;
    }
}

Note that this controller class contains room and date member variables that tell us which Room object to add the new Reservation to and what LocalDate to make the Reservation for, along with appropriate setter methods that we can use to set these member variables up correctly.

As you can see, the only significant bit of code in this controller class is in the acceptDialog method, which is linked to the Create button in the dialog's interface. That method makes a new Reservation object and then tries to add it to the Room. If the Room accepts the new Reservation, the acceptDialog method goes ahead and closes the dialog. If the Room rejects the Reservation, the method sets an error message in the interface and keeps the dialog open.

Here now is the code linked to the Add Reservation... menu item in the main window.

@FXML
private void addReservation(ActionEvent event) {
    Stage parent = (Stage) reservationsList.getScene().getWindow();

    try {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("FXMLNewEventDialog.fxml"));
        Parent root = (Parent)loader.load();
        Scene scene = new Scene(root);
        Stage dialog = new Stage();
        dialog.setScene(scene);
        dialog.setTitle("Create New Reservation");
        dialog.initOwner(parent);
        dialog.initModality(Modality.WINDOW_MODAL);
        dialog.initStyle(StageStyle.UTILITY);
        dialog.setX(parent.getX() + parent.getWidth()/4);
        dialog.setY(parent.getY() + parent.getHeight()/3);

        FXMLNewReservationDialogController controller = (FXMLNewReservationDialogController) loader.getController();
        controller.setRoom(currentRoom);
        controller.setDate(selectedDate);
        dialog.show();
    } catch(Exception ex) {
        System.out.println("Could not open dialog.");
        ex.printStackTrace();
    }
}

To create the dialog, we have to make a new Stage and Scene to hold the dialog interface. To load the interface for the dialog scene we make an FXMLLoader that can load the interface from the FXML file.

The second block of code above does some additional steps needed to make the new Stage we just created look and behave like a dialog. The first statement sets the main stage as the owner of the dialog stage. That way, if the user minimizes the main application window the dialog will get minimized along with it. The second statement makes the window containing the dialog a modal window. This means that users will not be able to interact with the main window until the dialog goes away. The last two statements in the block set the position of the dialog window relative to the main application window - this is purely to make the dialog pop up at a reasonable location on the screen.

The final block of code asks the FXMLLoader to go and fetch the controller object for the dialog. We need access to the dialog's controller so we can pass it the currently selected Room and date information.

Once everything is set up, we issue the show() command to the dialog's stage to make the dialog visible.