LibraryFX

Description

This demo program illustrates a JavaFX GUI front end for the LibraryDB project, which you should first install and test prior to creating this project. This project is created from scratch other than copying the models and setup packages into this one.

This application provides a graphical front end for the lending library of books and users. One might imagine an administrator using this to manage the "borrowing" and "returning" copies of available books. The quantity of the book represents how many copies are available. The features of the application are thus intended to deal with a subset of all possible interactions such as:

Remember to run Clean-Build

As a reminder, with projects such as these which rely on resource files, it can happen that the build gets "out of sync" with the source, and running the main file will not suffice to get a complete build. If a recent edit change does not seem to be in effect, remember to use Run ⇾ Clean and Build Main Project, or else the button.

Save changes frequently

NetBeans goes to some lengths to help you save your editing changes before running the application. However, there are no visual cues about saving changes in Scene Builder. Furthermore, you need to go back and forth between editing a Java file and editing an FXML file in Scene Builder.

It is best to compulsively Save changes made very frequently.

Understanding Coding Errors

When you use JavaFX, an error in your code can generate a voluminous amount of error information in the output. There is usually one key error line within the files of your application, select it to see the line which initially caused the error and fix it.

An error may be due to forgetting to assign an FXML data member to a corresponding fx:id in the FXML file. Please heed the maxim
Thou shalt Save and Test frequently
so that you have a better chance on knowing exactly what statements caused the error.

Watch out for fixed imports!

NetBeans has a great feature called "Fix Imports" which examines the classes used and generating import statements automatically. The problem is that there are some ambiguities of which package a class belongs to. For the most part, when there is a choice, choose the package starting with
javafx.
One of the most problematic ambiguity is for the Event class. The fix import choice lists several possibilities, including the one we want:
javafx.event.Event
If the others are used there will be no compiler error, and the runtime error message generated is essentially incomprehensible! Try to remember: If you get an incomprehensible error message, double-check the imports.

Binaries of Complete Program

For reference, download the binary versions of the completed program from this link:
LibraryFXbinary.zip
The archive LibraryFXbinary.zip is downloaded. Extracting it gives a directory with these files:
LibraryFXbinary/
  LibraryFX-mysql.jar
  LibraryFX-sqlite.jar
  database.sqlite
LibraryFX-mysql.jar is meant to work with the "standard" MySQL database in place, i.e., the test database accessible by the guest user with empty password. LibraryFX-mysql.jar is meant to work with the database.sqlite file.

On Windows or MAC you should be able to run by double-clicking the JAR file. Recent versions of MAC OS X take issue with this downloaded JAR file not being "properly signed", but you can get around that by right-clicking and selecting Open With ⇾ Jar Launcher. On Linux and other systems which have java available from the terminal shell command line you can run either:
java -jar LibraryFX-mysql.jar
java -jar LibarayFX-sqlite.jar
To initialize or reset the database table, make the appropriate database choice in
models.DBProps
and run the setup.MakeTables main class from the LibraryDB application. You can also run it from a terminal shell with either of these:
java -cp LibraryFX-mysql.jar setup.MakeTables
java -cp LibraryFX-sqlite.jar setup.MakeTables

LibraryFX Basis

  1. Select File ⇾ New Project and from that select the JavaFX category. Choose JavaFX FXML Application. Click Next.
  2. In the New JavaFX Application window, set the Project Name and then click Finish.
    Project Name: LibraryFX
We want to rename the "FXMLDocument" prefix to "Library" This could be done in two rename steps, but we'll show how a new FXML view file and associated controller are created from scratch.
  1. Select both FXMLDocument.fxml and FXMLDocumentController.java and Delete them from the right-click menu.
  2. Right-click on libraryfx package and select
    New ⇾ ( Other ⇾ JavaFX ⇾ ) Empty FXML
    • In the first dialog box, give it the FXML Name of Library. Click Next.
    • In the second dialog box, check the Use Java Controller checkbox. Click Finish.
    You should now have added the files
    Library.fxml
    LibraryController.java
    
    You can close Library.fxml since we never need to access it directly.
  3. Edit LibraryFX.java, change the resource file defining the root to Library.fxml:

    libraryfx.LibraryFX
    ...
    public class LibraryFX extends Application {
      @Override
      public void start(Stage stage) throws Exception {
        //======= change the FXML file named in this line:
        Parent root = FXMLLoader.load(getClass().getResource("Library.fxml"));
        ...
    Test-run the project to make sure the blank window comes up.
Double-click to open Library.fxml through Scene Builder. To do anything at all, you need a top level container; by default this is set to the easy-to-use AnchorPane, but we want to change this.
  1. Right-click on the AnchorPane in the Document ⇾ Hierarchy section and choose Delete.
  2. Select a BorderPane from Library ⇾ Containers and drag it onto the empty form. You can resize it, making it smaller. We will program the desired initial size.
  3. Open the Document ⇾ Controller section and set
    Controller Class
    libraryfx.LibraryController
    
    You should be able to enter this name through available selection.
Double-check that you've got it right by running the application. It should open an empty window.

Initial Component Layout

  1. Open the Library ⇾ Controls section, select a MenuBar and drag it onto the BorderPane form into the TOP section. As the MenuBar goes onto the form, the BorderPane layout will reveal the 5 available sections.
  2. Open the Library ⇾ Containers section, select a VBox and drag it onto the BorderPane form into the RIGHT section.
  3. Select a SplitPane (vertical) and drag it onto the form into the CENTER section.
When you're filling in components into a BorderPane, you can also drag them into the appropriate labeled positions within the Document ⇾ Hierarchy section: Insert TOP, Insert BOTTOM, Insert CENTER, Insert LEFT, and Insert RIGHT.

A SplitPane comes with two AnchorPane containers within. You'll want to replace these by a GridPane on top and a TextArea on the bottom.
  1. Open the Document ⇾ Hierarchy section and expand the SplitPane revealing the two AnchorPane containers. Select them both, right-click and Delete them.
  2. Open the Library ⇾ Containers section, select a GridPane and drag it onto the SplitPane in Document ⇾ Hierarchy section; it should drop within the SplitPane.
  3. Open the Library ⇾ Controls section, select a TextArea and drag it onto the SplitPane in Document ⇾ Hierarchy section; it should drop it within the SplitPane, below the previously added GridPane.
  4. Select the GridPane, and within it, select the bottom row. Right-click and Delete it, leaving a 2 × 2 Grid.
  5. Open the Library ⇾ Controls section and select ListView. Drag two ListViews into the bottom two cells of the GridPane.
  6. Select Label. Drag two Labels into the top two cells of the GridPane. Select and rename the Labels, respectively, as
    Books
    Users
    
  7. Select both Labels. In Inspector ⇾ Layout, set
    Halignment   
    
  8. Select the row containing the labels. In Inspector ⇾ Layout, set
    Vgrow        
    Pref Height  USE_COMPUTED_SIZE
    
The outcome is best seen from the Document ⇾ Hierarchy section.

Choose Preview ⇾ Show Preview to see what the app will look like (without any data). Compare that with the contents of the Document ⇾ Hierarchy section, getting something like this:

Make the SplitPane initial top size bigger than bottom

There's more going on in the lists than in the textarea, and so you can easily favor the amount of space for the top area as follows:
  1. Select the SplitPane component in Document ⇾ Hierarchy and then locate this setting in Inspector ⇾ Properties:
    Divider Position: 
    
  2. Change the value:
    Divider Position: 
    
Save the changes and the test-run to see the difference.

Engage the database

We will use the database code from the LibraryDB NetBeans project, which we are assuming you have installed from source. Technically, only the models package needs to be installed into LibraryFX, but you should install the setup and setup.tables packages as well so that the entire application is present.

The process is very easy in NetBeans. Open LibraryDB and open the source folder in the Projects window. From there, select all three packages:
models
setup
setup.tables
Right-click on any of these and choose Copy from the menu. Then open the source folder in LibraryFX, right-click on it and select Paste. Afterwards, you can close the LibraryDB project since it is no longer needed.

Add the SQL Libraries

In the Projects window for the LibraryFX project, right-click on Libraries.
  1. Add the MySQL driver JAR file as a project library:
    • Select Add Library.
    • Select MySQL JDBC Driver and click the Add Library button.
  2. Add the SQLite driver JAR file:
    • Select Add JAR/Folder.
    • Navigate to the sqlite-jdbc-3.16.1.jar file and click the Choose button.
Everything should work equally with either DBMS since we've already tested it. For definiteness I'll assume MySQL. Check models.DBProps and make the database selection be for MySQL. To ensure the build status, first run:
Clean and Build
Then create and populate the tables by running:
setup.MakeTables

Initialize the ORM, width, height and title

Add this code into the main class and fix the imports. This is the last time that we need to edit this file.

libraryfx.LibraryFX
...
public class LibraryFX extends Application {
  @Override
  public void start(Stage stage) throws Exception {
    Parent root = FXMLLoader.load(getClass().getResource("Library.fxml"));
 
    Scene scene = new Scene(root);
 
    stage.setScene(scene);
 
    //======= add these 3 lines
    stage.setWidth(700);
    stage.setHeight(500);
    stage.setTitle("LibraryFX - " + DBProps.which);  
    //=================
 
    stage.show();
  }
 
  public static void main(String[] args) {
    //====== add this try/catch block
    try {
      ORM.init(DBProps.getProps());      
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
    //================
    launch(args);
  }
}

Display books and users in lists

There are two parts, editing the controller and editing the view FXML file. You can do them in either order, but editing the controller first makes the "@FXML" objects available for selection in the Scene Builder when you edit the view file.

Edit the controller, adding code and fixing the imports to get the following controller content

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
 
  @FXML
  private ListView<Book> bookList;
 
  @FXML
  private ListView<User> userList;
 
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    try {
      Collection<Book> books = ORM.findAll(Book.class);
      for (Book book : books) {
        bookList.getItems().add(book);
      }
 
      Collection<User> users = ORM.findAll(User.class);
      for (User user : users) {
        userList.getItems().add(user);
      }
 
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }  
}
Edit Library.fxml using Scene Builder. You should be able to pick up these specifications through selection.
  1. Select the left ListView object. Open the Inspector ⇾ Code section and set
    fx:id     bookList
    
  2. Select the right ListView object. Open the Inspector ⇾ Code section and set
    fx:id     userList
    
Run the application. You should see the listing of books and users side-by-side. Here is an example with selections in both lists.
The default stylization of a list cell uses 3 color changes depending on:

Alter List Display Style

We want to manage the some display features through CSS. Create a new package, css, by right-clicking on the sources and selecting New ⇾ Java package. Fill in the texfield:
Package Name: 
Right-click on the css package and create a new stylesheet:
New ⇾ ( Other ⇾ Other ⇾ ) Cascading Style Sheet
Give it the name library-main, and enter this content:

css/library-main.css
.list-view {
  -fx-font-size: 11pt;
  -fx-font-family: sans-serif;
}
.list-cell:selected {
  -fx-background-color: #cbd;
  -fx-text-fill: black;
  -fx-font-weight:bold;
}
.list-view:focused .list-cell:selected {
  -fx-background-color: #ddc;
}
.text-area {
  -fx-font-size: 11pt;
}
.menu-bar {
  -fx-font-size: 11pt;
}
select
Engage this style sheet into the view by editing Library.fxml in Scene Builder. Choose the top-level BorderPane object within the Document ⇾ Hierarchy section. Then open Inspector ⇾ Properties. Towards the bottom look for
Stylesheets
+
Click on the "+" to open a file selection. Navigate to the newly-created library-main.css file and Open it. You should see an immediate change in the font size within the MenuBar as well as the textfield:
Stylesheets
@
../css/library-main.css
Run the application to observe the change in appearance with the modified style properties. The importance of this is to generate colors compatible with another one coming up.

Adding CSS files in future applications Once you've added a CSS file through Scene Builder, future additions will start from the file just added, so BE CAREFUL to navigate the browser to the right file in you current application! This is why I've branded the application name on the CSS file:
library-main.css
in order to emphasize that this file belongs to this application.

List cell rendering

The default text in a list cell is gotten using the object's toString function. Instead of changing that, we ultimately want something more dynamic, and so we need to establish a so-called "cell renderer" for our objects of interest.

Create the following Java classes within the library package using New ⇾ Java Class. Replace the contents by what is presented here and run Fix Imports to add the necessary imports.

libraryfx.BookCellCallback
package libraryfx;
 
class BookCellCallback implements Callback<ListView<Book>, ListCell<Book>> {  
  @Override
  public ListCell<Book> call(ListView<Book> p) {
    ListCell<Book> cell = new ListCell<Book>() {
      @Override
      protected void updateItem(Book book, boolean empty) {
        super.updateItem(book, empty);
        if (empty) {
          this.setText(null);
          return;
        }
        this.setText( book.getTitle() );
 
        // more code coming
      }
    };
    return cell;
  }
}
select

libraryfx.UserCellCallback
package libraryfx;
 
class UserCellCallback implements Callback<ListView<User>, ListCell<User>> {  
  @Override
  public ListCell<User> call(ListView<User> p) {
    ListCell<User> cell = new ListCell<User>() {
      @Override
      protected void updateItem(User user, boolean empty) {
        super.updateItem(user, empty);
        if (empty) {
          this.setText(null);
          return;
        }
 
        this.setText(user.getName());
 
        // more code coming
      }
    };
    return cell;
  }
}
select
Put these classes into play by augmenting the initialize function in LibraryController:

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
  ...  
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    try {	  
      Collection<Book> books = ORM.findAll(Book.class);
      for (Book book : books) {
        bookList.getItems().add(book);
      }
 
      Collection<User> users = ORM.findAll(User.class);
      for (User user : users) {
        userList.getItems().add(user);
      }
 
      //============= add these 4 statements:
      BookCellCallback bookCellCallback = new BookCellCallback();
      bookList.setCellFactory(bookCellCallback);
 
      UserCellCallback userCellCallback = new UserCellCallback();
      userList.setCellFactory(userCellCallback);      
    }
    catch(Exception ex) {
      ...
    }
  }  
}
Run to see the change in the book list cells. The display with selections looks something like this:

List selection handlers

When a user is selected, we want to indicate which books he/she has borrowed through a style change in bookList. Start by introducing a handler function for userlist selection. Edit the controller:

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
  ...
  //======= add this data member
  private Node lastFocused = null;
 
  //======= add these 2 handler functions:
  @FXML
  private void bookSelect(Event event) {
    Book book = bookList.getSelectionModel().getSelectedItem();
    if (book == null) {
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
      return;
    }
 
    lastFocused = bookList;
 
    System.out.println("bookList selection: " + book);
  }
 
  @FXML
  private void userSelect(Event event) {
    User user = userList.getSelectionModel().getSelectedItem();
    if (user == null) {
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
      return;
    }
    lastFocused = bookList;
 
    System.out.println("userList selection: " + user);
  }
  //============================
  ...
}
Get the right import for Event! When you fix the imports, this import seems to be the most error-prone because the Event class appears in multiple packages. Double-check that you have:
import javafx.event.Event;
There are three other wrong things you can get:
org.w3c.dom.events.Event
java.awt.event
sun.plugin2.ipc.Event
If you were to choose one of these three, there are likely to be no syntax errors, but a possibly indiscernable runtime error will show up.

We are not using the lastFocused member at this point. It will help refine our visual presentation later.

Edit Library.fxml in Scene Builder. Select the right ListView object, i.e., userList. Open the Inspector ⇾ Code section. Scroll down to the Mouse subsection, looking for On Mouse Clicked. Set it to the name of the handler function just created (you should be able to get it through selection):
On Mouse Clicked
#userSelect
Analogous thing for the left ListView object, i.e., bookList. Set the handler function:
On Mouse Clicked
#bookSelect
Run to make sure the list selections engage by the respective handler functions.

Show borrows on user selection

We first modify the BookCellCallback class so that it can "mark" certain books defined by a collection of ids. We have to first introduce an object which can hold the ids of books borrowed by the user.

libraryfx.BookCellCallback
...
class BookCellCallback implements Callback<ListView<Book>, ListCell<Book>> {  
 
  //======= add the following data member and setter (neither FXML)
 
  // the ids of books borrowed borrowed by a selected user
  private Collection<Integer> bookIds = null;
 
  void setBookIds(Collection<Integer> bookIds) {
    this.bookIds = bookIds;
  }
  //=====================
 
  @Override
  public ListCell<Book> call(ListView<Book> p) {
    ListCell<Book> cell = new ListCell<Book>() {
      @Override
      protected void updateItem(Book book, boolean empty) {
        super.updateItem(book, empty);
        if (empty) {
          this.setText(null);
          return;
        }
 
        this.setText(book.getTitle());
 
        //======== add this section at the end of the "updateItem" member 
        if (bookIds == null) {
          return;
        }
 
        String css = ""
            + "-fx-text-fill: #c00;"
            + "-fx-font-weight: bold;"
            + "-fx-font-style: italic;"
            ;
 
        if (bookIds.contains(book.getId())) {
          this.setStyle(css);
        }
        else {
          this.setStyle(null);
        }
        //====================
 
      }
    };
    return cell;
  }
}
Then edit LibraryController in 3 places. Make the special ids of booklist cells be the ids of the books borrowed by the selected user.

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
  ...
  //======= add this data member (not FXML)
  private final Collection<Integer> userBookIds = new HashSet<>();
 
  //======= replace userSelect by this version
  @FXML
  private void userSelect(Event event) {
    try {
      User user = userList.getSelectionModel().getSelectedItem();
      if (user == null) {
        if (lastFocused != null) {
          lastFocused.requestFocus();
        }
        return;
      }
      lastFocused = userList;
 
      Collection<Borrow> borrows = ORM.findAll(Borrow.class,
        "where user_id=?", new Object[]{user.getId()});
      userBookIds.clear();
      for (Borrow borrow : borrows) {
        userBookIds.add(borrow.getBookId());
      }
 
      // pick up style changes in booklist
      bookList.refresh();
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
  //======================
  ...
 
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    try {
      Collection<Book> books = ORM.findAll(Book.class);
      for (Book book : books) {
        bookList.getItems().add(book);
      }
 
      Collection<User> users = ORM.findAll(User.class);
      for (User user : users) {
        userList.getItems().add(user);
      }
 
      BookCellCallback bookCellCallback = new BookCellCallback();
      bookList.setCellFactory(bookCellCallback);
 
      UserCellCallback userCellCallback = new UserCellCallback();
      userList.setCellFactory(userCellCallback);
 
      //====== add this line
      bookCellCallback.setBookIds( userBookIds );
    }
    ...
  }  
}
Run the application to see the effect. On selecting a user, the user's borrowed books are now designated by color/bold text style like this:

Text Display

On selecting a user or book we want the full information to be displayed in the TextArea. Create a new Java class, Helper, in the libraryfx package for static helper functions. The access qualifiers on everthing in this class can be "none" since we only want to use the functions within the libraryfx package.

libraryfx.Helper
package libraryfx;
 
import models.Book;
import models.User;
 
class Helper {
  static String bookInfo(Book book) {
    return String.format(
        "id: %s\n"
        + "title: %s\n"
        + "binding: %s\n"
        + "quantity: %s\n",
        book.getId(),
        book.getTitle(),
        book.getBinding(),
        book.getQuantity()
    );
  }
 
  static String userInfo(User user) {
    return String.format(
        "id: %s\n"
        + "name: %s\n"
        + "email: %s\n",
        user.getId(),
        user.getName(),
        user.getEmail()
    );
  }
 
  static java.sql.Date currentDate() {
    long now = new java.util.Date().getTime();
    java.sql.Date date = new java.sql.Date(now);
    return date;
  }
}
select
Edit the controller. Add the display data member, and modify the userSelect handler, add the bookSelect handler:

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
  ...
  // add this data member
  @FXML
  TextArea display;
 
  // add 1 line into userSelect and bookSelect
  @FXML
  private void bookSelect(Event event) {
    ...
    //======= add this line at end of function
    display.setText(Helper.bookInfo(book));
  }
 
  @FXML
  private void userSelect(Event event) {
    try {
      User user = userlist.getSelectionModel().getSelectedItem();
      ...
      //======= add this line at end of try block
      display.setText(Helper.userInfo(user));
    }
    catch (Exception ex) {
      ...
    }
  }
 
  ... 
}
The display member references the TextArea type. When you fix the imports, make sure it is the JavaFX version, not the Swing version.

Edit Library.fxml through Scene Builder.
  1. Select the TextArea. Open the Inspector ⇾ Properties section. Uncheck the Editable checkbox:
    Editable      
    
  2. Open the Inspector ⇾ Code section. Set the fx:id to display:
    fx:id        display
    
Run the application, making user and book selections to see the effects, something like this:
 
Also verify that the display textarea is not editable.

Buttons

We'll now introduce buttons which will allow us to display and manipulate the user/book borrow status, and clear the GUI. Add these button handler functions into the controller. The clear handler is complete and the other three are starter stubs.

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
  ...
  //==================
  @FXML
  private void bookUserStatus(Event event) {
    System.out.println("bookUserStatus");
  }
 
  @FXML
  private void bookReturn(Event event) {
    System.out.println("bookReturn");
  }
 
  @FXML
  private void bookBorrow(Event event) {
    System.out.println("bookBorrow");
  }
 
  @FXML
  private void clear(Event event) {
    userList.getSelectionModel().clearSelection();
    bookList.getSelectionModel().clearSelection();
    userBookIds.clear();
    bookList.refresh();
    display.setText("");
  }
  //==================
 
  ...
}
Edit Library.fxml through Scene Builder.
  1. Open the Library ⇾ Controls section, drag 4 Buttons into the VBox on the right side. Select and rename respectively, from top to bottom:
    
    
    
    
    
  2. Select the VBox. Open the Inspector ⇾ Layout section. Make these settings:
    Padding      5 5 5 5
    Spacing      5
    ...
    Pref Width   USE_COMPUTED_SIZE
    
  3. Open the Inspector ⇾ Code section. One-by-one, go through the buttons, top to bottom, setting the On Action handler value:
    #bookUserStatus
    #bookReturn
    #bookBorrow
    #clear
    
    You should be able to select these from the list of available handlers.
Run the application. Check that each button has the expected effect on clicking. Here is the intended appearance:
You can test the Clear button which works without any changes.

Multi-line Button text

You can easily make multi-line Button labels in JavaFX8. For example, we now have the button with this appearance:
Let's say we want more information in its text:
In order to achieve this, select the Status button, and go to the Inspector ⇾ Properties section where you see
Text   Status
Select the small icon to the right of the box and click to reveal the choices:
Reset to default
Replace with Internationalized String
Switch to multi-lines mode
Choosing the last one opens up the box:
Text   
Status

Then simply change the content to:
Text   
Borrow
Status

Button Functionality

The Status, Return, Borrow buttons have in common that both a book and user must be selected. Additionally,

Expected Exception and Alerts

If the requirements for button usage are not met, we regard it as a simple user mistake which invokes an informational popup dialog telling the user that certain conditions are not met.

We want to handle mistakes by throwing an exception, and want to separate the expected user exceptions from other exceptions generated by errors due to coding, database handling, etc. Toward this end, we introduce a new class.

Right-click on the libraryfx package and create the following New ⇾ Java Class:

libraryfx.ExpectedException
package libraryfx;
 
class ExpectedException extends Exception {
  ExpectedException(String message) {
    super(message);
  }
}
select
Once an expected exception is detected, the informational popup presented is called an Alert. JavaFX8 provides many possibilities for Alerts and other Dialogs. Here is a sample introductory tutorial
JavaFX Dialogs @ code.makery
The simplest usage, which we have employed below to display the exception's message is this:
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setContentText(ex.getMessage());
alert.show();
Its behavior is intended to be very much like the alert function in JavaScript.

Book Status

Edit LibraryController. Replace the bookUserStatus handler function with the following final version

libraryfx.LibraryController
  private void bookUserStatus(Event event) {    
    try {
      User user = userList.getSelectionModel().getSelectedItem();
      Book book = bookList.getSelectionModel().getSelectedItem();
      if (user == null || book == null) {
        throw new ExpectedException("must select book and user");
      }
      Borrow borrow = ORM.findOne(Borrow.class, 
        "where user_id=? and book_id=?", 
        new Object[]{user.getId(), book.getId()}
      );
 
      if (borrow == null) {
        throw new ExpectedException("user does not have book");
      }
 
      display.setText("borrowed on: " + borrow.getBorrowedAt());
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
Run the application to test it. Check all possibilities: This is the first time we've made use of the lastFocused member. The idea is that if they user/book pair selected does not represent a borrow, then, after handling the alert, we'll go back exactly to the state prior to invoking the Status button.

The goal is to keep the application presentation consistent with itself as much as possible.

Book Return

Replace the bookReturn handler function with the following final version:

libraryfx.LibraryController
  private void bookReturn(Event event) {
    try {
      User user = userList.getSelectionModel().getSelectedItem();
      Book book = bookList.getSelectionModel().getSelectedItem();
      if (user == null || book == null) {
        throw new ExpectedException("must select book and user");
      }
 
      // get the book from database
      Borrow borrow = ORM.findOne(Borrow.class, 
        "where user_id=? and book_id=?", 
        new Object[]{user.getId(), book.getId()}
      );
      if (borrow == null) {
        throw new ExpectedException("user does not have the book");
      }
      // remove the borrow
      ORM.remove(borrow);
      book.setQuantity(book.getQuantity()+1);
      ORM.store(book);
 
      // reset booklist
      userBookIds.remove(book.getId());
      bookList.refresh();
      bookList.requestFocus();
      display.setText(Helper.bookInfo(book));
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
Run the application, choosing a book/user borrow pair, clicking the button and observing the effect.

One difference of this from status is the code necessary to effect the return of the book by removing the borrow record and changing the quantity. The displays in the booklist and the display must be changed. A successful return always presents the book selection because we want to show the change in quantity.

Book Borrow

Replace the bookBorrow member function by this:

libraryfx.LibraryController
  private void bookBorrow(Event event) {
    try {
      User user = userList.getSelectionModel().getSelectedItem();
      Book book = bookList.getSelectionModel().getSelectedItem();
      if (user == null || book == null) {
        throw new ExpectedException("must select book and user");
      }
      Borrow borrow = ORM.findOne(Borrow.class, 
        "where user_id=? and book_id=?", 
        new Object[]{user.getId(), book.getId()}
      );
      if (book.getQuantity() == 0) {
        throw new ExpectedException("no copies remaining");        
      }
      if (borrow != null) {
        throw new ExpectedException("user already has the book");
      }
      borrow = new Borrow(book, user, Helper.currentDate());
      ORM.store(borrow);
      book.setQuantity(book.getQuantity()-1);
      ORM.store(book);
      userBookIds.add(book.getId());
      bookList.refresh();
      bookList.requestFocus();
      display.setText(Helper.bookInfo(book));
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
The bookBorrow operation is the most involved of the three because it requires testing of the book's quantity as well as setting the current date in the borrow join record.

Run the application, choosing a non-borrow book/user pair and click the Borrow button. Continuing on, make 4 of the 5 users all borrow the book and then attempt borrowing by the 5th user to observe the failure.

Prepare Menu usage

Edit Library.fxml through Scene Builder. We want to modify the contents of the MenuBar. Within the Document ⇾ Hierarchy section, you can rename existing Menus and MenuItems, and/or, you can add new MenuItems from the Library ⇾ Menu section by dragging them onto a specific Menu. The outcome should look like this in the Document ⇾ Hierarchy section:
A preview gives these intended appearances:

Assign handlers to the menu items

Add these menu handler function stubs into the LibraryController:

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
  ...
 
  //=======
  @FXML
  private void removeUser(Event event) {
    System.out.println("removeUser");
  }
 
  @FXML
  private void addBook(Event event) {
    System.out.println("addBook");
  }
 
  @FXML
  private void modifyBook(Event event) {
    System.out.println("modifyBook");    
  }
  //=======
 
  ...
}
Edit Library.fxml in Scene Builder. One-by-one, select each of the 3 menu items within Document ⇾ Hierarchy; open Inspector ⇾ Code and set On Action (you should be able to select it):
menu item         handler function
Books ⇾ Add       addBook
Books ⇾ Modify    modifyBook
Users ⇾ Remove    removeUser
Run the application and test each menu item to verify the outcome.

Make key LibraryContoller member accessible

We will need to access these fields within the dialogs we are creating.

libraryfx.LibraryController
package libraryfx;
...
public class LibraryController implements Initializable {
 
  //======= just below the data members, add these 3 getters:
  ListView<Book> getBookList() {
    return bookList;
  }
 
  ListView<User> getUserList() {
    return userList;
  }
 
  TextArea getDisplay() {
    return display;
  }
  //===============
  ...
}
It's up to you whether to make these getters public or not. As is, we only ever plan to use then within the "controller" classes which we plan to keep within the same package.

Create AddBook Dialog

Like many complex features in a GUI application, it is better to create separate GUI windows for adding and modifying the fields of books and users. These sub-GUI windows are called dialogs. A dialog normally operates in a "modal" style where, upon being invoked, makes it impossible to do anything else until it is shut down.

We're going to create a new FXML/Controller pair just as we have done for the original one. Right-click on libraryfx package and select
New ⇾ ( Other ⇾ JavaFX ⇾ ) Empty FXML
You should now have added the files:
AddBook.fxml
AddBookController.java
We now have two controllers to work with. The actions of adding a book are to be done in the AddBookController.

Understand how to invoke the dialog

We already have the menus set up and the stubs for the functions to be called in the LibraryController. The job of the addBook function in LibraryController is to invoke the dialog. Replace the stub for addBook in LibraryController by this initial code:

libraryfx.LibraryController (initial)
  private void addBook(Event event) {
    try {
      //======================== The absolutely essential part
      // get fxmlLoader
      URL fxml = getClass().getResource("AddBook.fxml");
      FXMLLoader fxmlLoader = new FXMLLoader(fxml);
      fxmlLoader.load();
 
      // get scene from loader
      Scene scene = new Scene(fxmlLoader.getRoot());
 
      // create a stage for the scene
      Stage dialogStage = new Stage();            
      dialogStage.setScene(scene);
 
      // specify dialog title
      dialogStage.setTitle("Add a Book");
 
      // make it block the application
      dialogStage.initModality(Modality.APPLICATION_MODAL);
 
      // invoke the dialog
      dialogStage.show();
      //===============================================   
    }
    catch (IOException ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
Note the following statement which relates the dialog generated to the associated FXML view file. Everything else is generic.
URL fxml = getClass().getResource("AddBook.fxml");
Run the application and invoked the dialog from the Books ⇾ Add menu item. Verify that the dialog is modal by attempting to access the main application when the dialog is active (and failing).

Make LibraryController available to AddBookController

Edit AddBookController. We want to create a data member which gives access to the LibraryController itself, assigned when the dialog is invoked.

libraryfx.AddBookController
package libraryfx;
...
public class AddBookController implements Initializable {
 
  //===================== add this data member and setter
  private LibraryController mainController;
 
  void setMainController(LibraryController mainController) {
    this.mainController = mainController;
  }
  //=====================================
 
  ...
}
Edit LibraryController, replacing the initial addBook handler by this, which adds two more statements:

libraryfx.LibraryController
  private void addBook(Event event) {
    try {
      //======================== The absolutely essential part
      // get fxmlLoader
      URL fxml = getClass().getResource("AddBook.fxml");
      FXMLLoader fxmlLoader = new FXMLLoader(fxml);
      fxmlLoader.load();
 
      // get scene from loader
      Scene scene = new Scene(fxmlLoader.getRoot());
 
      // create a stage for the scene
      Stage dialogStage = new Stage();            
      dialogStage.setScene(scene);
 
      // specify dialog title
      dialogStage.setTitle("Add a Book");
 
      // make it block the application
      dialogStage.initModality(Modality.APPLICATION_MODAL);
 
      // invoke the dialog
      dialogStage.show();
      //============================================
 
      // these 2 steps are usually, but not always necessary
 
      // get controller from fxmlLoader (must be defined in FXML file)
      // you will need this if you want to set fields before invocation
      AddBookController dialogController = fxmlLoader.getController();
 
      // pass the LibraryController to the dialog controller
      // so that the dialog can affect changes in the main window
      dialogController.setMainController(this);
 
      //============ additional features
 
    }
    catch (IOException ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
Note the specific named usage of the class AddBookController. This and the AddBook.fxml represent the usage of the two entities created for the dialog.

Re-run the application and test the dialog.

Edit AddBook.fxml

We want to make this dialog be a simple grid of rows, each containing a label and a control component allowing data entry. The last row consists of buttons to manage the submission.

Edit AddBook.fxml through Scene Builder.
  1. Delete the initial AnchorPane.
  2. Drag a GridPane onto the form. Resize to make it smaller.
  3. Open Document ⇾ Controller and set
    Controller class
    libraryfx.AddBookController
    
    It should be available through selection.
Confirm Dialog Invocation These 3 steps are the most basic. Before continuing, run the application to verify that it the dialog comes up correctly. If you have omitted step 3, this statement will give a null pointer exception:
dialogController.setMainController(this);
  1. From Library ⇾ Controls, drag 3 Labels into the left-hand column of each row, rename them as follows:
    title:
    binding:
    quanity:
    
  2. Opposite of these 3, in the right-hand column, drag these Control components:
    TextField
    ComboBox
    TextField
    
  3. Select and right-click on the last row. Choose Add Row Below, creating row 3. Drag an HBox container into right-hand cell of row 3.
  4. Drag two Buttons into the newly-created HBox. Rename the buttons:
      
    
  5. In the Inspector ⇾ Layout section for the HBox, set:
    Spacing 10
    
  6. Select all 4 rows. From Inspector ⇾ Layout, set
    Vgrow     NEVER
    
  7. Select the left column. From Inspector ⇾ Layout, set
    Hgrow     NEVER
    
  8. Select the top level GridPane (de-selecting all rows). From Inspector ⇾ Layout, set
    Vgap       10
    Padding    10 10 10 10
    
  9. Select the left column. From Inspector ⇾ Layout, set
    Pref Width    USE_COMPUTED_SIZE
    
Run preview. The appearance should be something like this:
When you expand the window, the textfields expand horizontally and empty space is created vertically.

AddBook Dialog initial controller code

Edit the AddBook, subsituting this initial class content and fixing the imports:

libraryfx.AddBookController
public class AddBookController implements Initializable {
 
  private LibraryController mainController;
 
  void setMainController(LibraryController mainController) {
    this.mainController = mainController;
  }
 
  @FXML
  private TextField titleField;
 
  @FXML
  private ComboBox<String> bindingSelection;
 
  @FXML
  private TextField quantityField;
 
  TextField getTitleField() {
    return titleField;
  }
 
  ComboBox<String> getBindingSelection() {
    return bindingSelection;
  }
 
  TextField getQuantityField() {
    return quantityField;
  }
 
  //======= initial version of add
  @FXML
  private void add(Event event) {
    String binding = bindingSelection.getSelectionModel().getSelectedItem();
    String title = titleField.getText().trim();
    String quantityStr = quantityField.getText().trim();
 
    System.out.format("addBook: %s,%s,%s\n", binding, title, quantityStr);
 
    //((Button)event.getSource()).getScene().getWindow().hide();
  }
 
  @FXML
  private void cancel(Event event) {
    ((Button)event.getSource()).getScene().getWindow().hide();
  }
 
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    bindingSelection.getItems().add("paper");
    bindingSelection.getItems().add("cloth");    
    bindingSelection.setValue("paper");  // pre-set
  }    
}
select
Next, edit AddBook.fxml in Scene Builder. Open Inspector ⇾ Code.
  1. One-by-one, establish the fx:id's for the 3 components in the right-hand column (top to bottom):
    component    fx:id
    TextField    titleField
    ComboBox     bindingSelection
    TexField     quantityField
    
  2. One-by-one, specify the On Action handlers for the buttons:
    button    On Action
    Add       add
    Cancel    cancel
    
Run the application, set arbitrary field values to verify that these values are getting recognized in add and that the dialog is closed by cancel. Observe how we close a dialog through the call to:
((Button)event.getSource()).getScene().getWindow().hide();
Another way to close the window is by the window manager "close" icon, a feature which we need to manage as well.

AddBook Dialog Operation

Validate the added book

Start programming the adding of a book by validating the fields set by the user. Replace the current add function by this:

libraryfx.AddBookController
  private void add(Event event) {
    try {
      String title = titleField.getText().trim();
      String binding = bindingSelection.getSelectionModel().getSelectedItem();
      String quantityStr = quantityField.getText().trim();
 
      if (title.length() < 3) {
        throw new ExpectedException("title length must be at least 3");
      }
      if (!quantityStr.matches("\\d+")) {
        throw new ExpectedException("quantity must be a non-negative integer");
      }
      int quantity = Integer.valueOf(quantityStr);
 
      Book bookWithTitle
          = ORM.findOne(Book.class, "where title=?", new Object[]{title});
      if (bookWithTitle != null) {
        throw new ExpectedException("existing book with same title");
      }
      // validation OK
      System.out.println("Valiation: OK");
 
      // more coming
 
      // ((Button) event.getSource()).getScene().getWindow().hide();
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
Run the applications. Test various input values to trigger each possible exception. Test the title uniqueness by setting it to an existing one, like "Machine Learning."

Complete the add code

Actually adding the book means that we want to put it into the book database table and also put it into the booklist.

libraryfx.AddBookController
  private void add(Event event) {
    try {
      String title = titleField.getText().trim();
      String binding = bindingSelection.getSelectionModel().getSelectedItem();
      String quantityStr = quantityField.getText().trim();
 
      if (title.length() < 3) {
        throw new ExpectedException("title length must be at least 3");
      }
      if (!quantityStr.matches("\\d+")) {
        throw new ExpectedException("quantity must be a non-negative integer");
      }
      int quantity = Integer.valueOf(quantityStr);
 
      Book bookWithTitle
          = ORM.findOne(Book.class, "where title=?", new Object[]{title});
      if (bookWithTitle != null) {
        throw new ExpectedException("existing book with same title");
      }
      // validation OK
 
      // access the features of LibraryController
      ListView<Book> bookList = mainController.getBookList();
      TextArea display = mainController.getDisplay();
 
      // put it into the database
      Book newBook = new Book(title, binding, quantity);
      ORM.store(newBook);
 
      // reload booklist from database
      bookList.getItems().clear();
      Collection<Book> books = ORM.findAll(Book.class);
      for (Book book : books) {
        bookList.getItems().add(book);
      }
 
      // select in list and scroll to added book
      bookList.getSelectionModel().select(newBook);
      bookList.scrollTo(newBook);
      bookList.requestFocus();
 
      // set text display to added book
      display.setText(Helper.bookInfo(newBook));
 
      ((Button) event.getSource()).getScene().getWindow().hide();
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
You now see the importance of getting access to the LibraryController object, mainController so that we can manipulate the display members before returning.

Although we could add the new book directly into the booklist, the order of the books could potentially be a relevant feature, in which case it serves best to clear the list and retrieving them in the specified order.

Query window closing

Closing the dialog window through the window manager "close" icon represents an alternative way of aborting the add operation. Doing so however might be an "unintended" action, and so we want to query the user whether to really close or not.

Unfortunately, the FXML usage does not appear to offer a way to handle window closing, and so we have to rely directly on using JavaFX code. Here we're using a confirmation alert popup. Add the following indicated code, fixing the imports.

libraryfx.LibraryController
  private void addBook(Event event) {
    try {
      //======================== The absolutely essential part
      // get fxmlLoader
      URL fxml = getClass().getResource("AddBook.fxml");
      FXMLLoader fxmlLoader = new FXMLLoader(fxml);
      fxmlLoader.load();
 
      // get scene from loader
      Scene scene = new Scene(fxmlLoader.getRoot());
 
      // create a stage for the scene
      Stage dialogStage = new Stage();            
      dialogStage.setScene(scene);
 
      // specify dialog title
      dialogStage.setTitle("Add a Book");
 
      // make it block the application
      dialogStage.initModality(Modality.APPLICATION_MODAL);
 
      // invoke the dialog
      dialogStage.show();
      //====================================================
 
      // these 2 steps are usually, but not always necessary
 
      // get controller from fxmlLoader (must be defined in FXML file)
      // you will need this if you want to set fields before invocation
      AddBookController dialogController = fxmlLoader.getController();
 
      // pass the LibraryController to the dialog controller
      // so that the dialog can affect changes in the main window
      dialogController.setMainController(this);
 
      //============ additional features
 
      // avoid too small horizontal size
      dialogStage.setMinWidth(250);
 
      // query window closing
      dialogStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
        @Override
        public void handle(WindowEvent event) {
          Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
          alert.setContentText("Are you sure you want to exit this dialog?");
          Optional<ButtonType> result = alert.showAndWait();
          if (result.get() != ButtonType.OK) {
            event.consume();
          }
        }
      });
 
    }
    catch (IOException ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select

Using seeded data

For easy testing of the behavior of valid book adds, we can "seed" the fields with (most likely) valid data when the dialog pops up. Edit the addBook function in the main controller:

libraryfx.LibraryController
  private void addBook(Event event) {
    try {
      ...
 
      //====== append this to the end of the try block: seed data
      dialogController.getTitleField().setText(
          "Test_" + new java.util.Random().nextInt(1000));
      dialogController.getQuantityField().setText("4");
 
    }
    catch (IOException ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
  ...
Run the application. Bring up the add book dialog. Most likely you will be able to add a book from the data set since the title is very likely to be different each time.

After testing the behavior of adding valid books, comment it out the seeding code.

Remove a User

The ability to remove a user actually requires the removal of all the join records, meaning that the user has given back all borrowed books before being removed. It's the foreign key constraints in the borrow table which prevent removal of a book prior to removal of the records which reference it.

Replace the removeUser stub function by this:

libraryfx.LibraryController
  private void removeUser(Event event) {
    try {
      User user = userList.getSelectionModel().getSelectedItem();
      if (user == null) {
        throw new ExpectedException("must select user");
      }
 
      // find all the books borrowed by the user
      Collection<Borrow> borrows = ORM.findAll(Borrow.class,
          "where user_id=?", new Object[]{user.getId()});
 
      // cannot remove, still has borrows
      if (!borrows.isEmpty()) {
        throw new ExpectedException("user must return borrowed books");
      }
 
      Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
      alert.setContentText("Are you sure?");
      Optional<ButtonType> result = alert.showAndWait();
      if (result.get() != ButtonType.OK) {
        return;
      }
 
      // remove from user table
      ORM.remove(user);
 
      // remove from list
      userList.getItems().remove(user);
      userList.getSelectionModel().clearSelection();
 
      // if book is selected, display it
      Book book = bookList.getSelectionModel().getSelectedItem();
      if (book != null) {
        display.setText(Helper.bookInfo(book));
        bookList.requestFocus();
      }
      else {
        display.setText("");
      }
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
In contrast to adding an object, removing one is simpler in the sense that we remove it from the database table and remove from the list; there is no need to reload the table.

The user's books must be returned to remove all the relevant entries from the borrow tables. This is enforced by the foreign key constraints set on the borrow table. Observe the code failure if you attempt to remove a user without returning his/her books. Comment out the line:
//      throw new ExpectedException("user must return borrowed books");
Then run and attempt to remove a user without returning the books. Look for the error in output:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: 
Cannot delete or update a parent row: a foreign key constraint fails ...
Remove the comment after testing.

Disable the Remove menu item if no selected user

Compared to a button, using a menu item for activation provides a relatively simple way to disable the menu item if it cannot be used. The point is that the menu containing the menu item must be activated first, at which point we can determine whether to enable or disable the menu item(s) it contains.

Add this member and member function to the LibraryController (fixing the imports):

libraryfx.LibraryController
  ...
  @FXML
  private MenuItem removeUserMenuItem;
  ...
  @FXML
  private void activateUsersMenu(Event event) {
    User user = userList.getSelectionModel().getSelectedItem();
    removeUserMenuItem.setDisable( user == null );
  }
  ...
Then edit LibraryFX.fxml in SceneBuilder and make these changes:
  1. Select the Users Menu and set (through selection):
    On Shown
    activateUsersMenu
    
    It appears that "On Showing" or "On Shown" both work. However "On Action" does not work.
  2. Within the UsersMenu, select the Remove MenuItem and set
    fx:id  removeUserMenuItem
    
Run the app to confirm. The Remove MenuItem is only available if a User is selected.

Modify a Book

Modification (Update) is usually the most complex of the standard CRUD operations. Like Delete, we start with a selected object. Then, we must Read the object and present it to the user in fields much like what happens with Create, giving the user the opportunity to change one or more of these. The choice of which fields are modifiable depends on the needs of the application. In this application we'll show how to modify of all the book fields, although normally we might consider that the title is synonymous with the book itself and should not be modifiable.

This dialog we want is so close to add that we can copy it instead of creating it from scratch.
  1. Copy/Paste AddBookController.java to a new (refactored) Java class ModifyBookController.java.
  2. Copy/Paste/Rename AddBook.fxml to a new file ModifyBook.fxml.
  3. Edit ModifyBook.fxml in Scene Builder. Connect the correct controller. Open Document ⇾ Controller and change
    Controller Class
    libraryfx.ModifyBookController
    
Then edit the new dialog controller. Add another Replace add by the following modify function:

libaryfx.ModifyBookController
 
  // add this data member and setter
  private Book bookToModify;
 
  void setBookToModify(Book bookToModify) {
    this.bookToModify = bookToModify;
  }
 
  // replace the add function by this:
  @FXML
  private void modify(Event event) {
    try {
      String binding = bindingSelection.getSelectionModel().getSelectedItem();
      String title = titleField.getText().trim();
      String quantityStr = quantityField.getText().trim();
 
      // access the features of LibraryController
      ListView<Book> booklist = mainController.getBookList();
      TextArea display = mainController.getDisplay();
 
      if (title.length() < 3) {
        throw new ExpectedException("title length must be at least 3");
      }
      if (!quantityStr.matches("\\d+")) {
        throw new ExpectedException("quantity must be a non-negative integer");
      }
      int quantity = Integer.valueOf(quantityStr);
 
      Book bookWithTitle
          = ORM.findOne(Book.class, "where title=?", new Object[]{title});
 
      // check that there is not a DIFFERENT book with same title
      if (bookWithTitle != null && bookWithTitle.getId() != bookToModify.getId()) {
        throw new ExpectedException("another book with same title");
      }
 
      // validation OK
      System.out.println("validation OK");
 
      // make modifications and put it into the database
      bookToModify.setTitle(title);
      bookToModify.setBinding(binding);
      bookToModify.setQuantity(quantity);
      ORM.store(bookToModify);
 
      // make modification in place in booklist
      int index = booklist.getSelectionModel().getSelectedIndex();
      booklist.getItems().set(index, bookToModify);
      booklist.requestFocus();
 
      // set text display to modified book information
      display.setText(Helper.bookInfo(bookToModify));
 
      ((Button) event.getSource()).getScene().getWindow().hide();
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
    }
    catch (Exception ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
With the modify function in place, edit ModifyBook.fxml in Scene Builder: Finally, set the modifyBook method in LibraryController:

libraryfx.LibraryController
  private void modifyBook(Event event) {
    try {
      Book book = bookList.getSelectionModel().getSelectedItem();
      if (book == null) {
        throw new ExpectedException("must choose a book");
      }
      //======================== The absolutely essential part
      // get fxmlLoader
      URL fxml = getClass().getResource("ModifyBook.fxml");
      FXMLLoader fxmlLoader = new FXMLLoader(fxml);
      fxmlLoader.load();
 
      // get scene from loader
      Scene scene = new Scene(fxmlLoader.getRoot());
 
      // create a stage for the scene
      Stage dialogStage = new Stage();
      dialogStage.setScene(scene);
 
      // specify dialog title
      dialogStage.setTitle("Modify a Book");
 
      // make dialog block the application
      dialogStage.initModality(Modality.APPLICATION_MODAL);
 
      // invoke the dialog
      dialogStage.show();
      //====================================================
 
      // get controller from fxmlLoader (must be defined in FXML file)
      // you will need this if you want to set fields before invocation
      ModifyBookController dialogController = fxmlLoader.getController();
 
      // pass the LibraryController to the dialog controller
      // so that the dialog can affect changes in the main window 
      dialogController.setMainController(this);
 
      //============ additional features
 
      // avoid too small
      dialogStage.setMinWidth(200);
 
      // query window closing
      dialogStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
        @Override
        public void handle(WindowEvent event) {
          Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
          alert.setContentText("Are you sure you want to exit this dialog?");
          Optional<ButtonType> result = alert.showAndWait();
          if (result.get() != ButtonType.OK) {
            event.consume();
          }
        }
      });
 
      // seed the fields with the values of the selected book
      dialogController.getTitleField().setText(book.getTitle());
      dialogController.getQuantityField().setText(
          String.valueOf(book.getQuantity())
      );
      dialogController.getBindingSelection().setValue(book.getBinding());
 
      // set book to be modified in dialog
      dialogController.setBookToModify(book);
    }
    catch (ExpectedException ex) {
      Alert alert = new Alert(Alert.AlertType.INFORMATION);
      alert.setContentText(ex.getMessage());
      alert.show();
      if (lastFocused != null) {
        lastFocused.requestFocus();
      }
    }
    catch (IOException ex) {
      ex.printStackTrace(System.err);
      System.exit(1);
    }
  }
select
Run the application. The new validation failure to observe happens if you attempt to change the title to that of another book.

Modify Validation

The validation is like that in addBook, except for the determination of a duplicate title. Both add and modify look for a book with given title:
Book bookWithTitle 
  = ORM.findOne(Book.class, "where title=?", new Object[]{title});
When adding, we fail if there is such a book:
if (bookWithTitle != null) { /* invalid */ }
When modifying, we fail if there is a book with the same title which is not the one to be modified:
if (bookWithTitle != null &&
    bookWithTitle.getId() != bookToModify.getId()) { /* invalid */ }


© Robert M. Kline