Java Quiz Player

A Reminder App using JavaFX - Java Source Code

AppGui.java

package com.javaquizplayer.examples.reminderapp;

import javafx.scene.Parent;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.scene.paint.Color;
import javafx.scene.control.TableView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.ListView;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Dialog;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import jfxtras.scene.control.LocalDatePicker;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;
import javafx.beans.property.SimpleBooleanProperty;
import javax.annotation.PostConstruct;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Optional;
import java.util.TimerTask;
import java.util.Timer;

import org.springframework.dao.DataAccessException;


/*
 * Class builds the GUI for the app.
 * Gets the app's data from the database and shows in the reminders table.
 * Starts a Timer and schedules its reminder notification task.
 * Has functions to add, delete and update reminders.
 */
public class AppGui {


    private TableView<Reminder> table;
    private ListView<String> list;
    private LocalDatePicker localDatePicker;
    private Text actionStatus;
	
    private HBox hbPane;
    private Timer timer;
    private DataAccessException dataAccessException;

    private DataAccess dataAccess;
    private CheckRemindersTask remindersTask;
    private ReminderDialog reminderDialog;
	

    /*
     * Constructor.
     * Constructs the app's GUI.
     */
    public AppGui(DataAccess dataAccess, CheckRemindersTask remindersTask, ReminderDialog reminderDialog) {
	
        this.dataAccess = dataAccess;
        this.remindersTask = remindersTask;
        this.reminderDialog = reminderDialog;

        // List view and calendar
        list = new ListView<String>(ReminderGroup.getAsFormattedStrings());
        list.getSelectionModel().selectedIndexProperty().addListener(
                new ListSelectChangeListener());		
        localDatePicker = new LocalDatePicker();
		
        // VBox with list and calendar
        VBox vbox1 = new VBox(15);
        vbox1.getChildren().addAll(list, localDatePicker);

        // Buttons in a hbox
        Button newBtn = new Button("New");
        newBtn.setOnAction(actionEvent -> newReminderRoutine());
        Button updBtn = new Button("Update");
        updBtn.setOnAction(actionEvent -> updateReminderRoutine());
        Button delBtn = new Button("Delete");
        delBtn.setOnAction(actionEvent -> deleteReminderRoutine());
		
        HBox btnHb = new HBox(10);
        btnHb.getChildren().addAll(newBtn, updBtn, delBtn);
		
        // Table view, columns and properties (row and column)	
        table = new TableView<Reminder>();
		
        TableColumn<Reminder, String> nameCol = new TableColumn<>("Name");
        nameCol.setCellValueFactory(new PropertyValueFactory<Reminder, String>("name"));		
        nameCol.setMaxWidth(1f * Integer.MAX_VALUE * 45);  // 45% of table width
		
        TableViewRowAndCellFactories cellFactories = new TableViewRowAndCellFactories();

        TableColumn<Reminder, LocalDate> dateCol = new TableColumn<>("Date");
        dateCol.setMaxWidth(1f * Integer.MAX_VALUE * 15);
        dateCol.setCellValueFactory(new PropertyValueFactory<Reminder, LocalDate>("date"));
        dateCol.setCellFactory(column -> cellFactories.getTableCellWithDateFormatting());	
		
        TableColumn<Reminder, LocalTime> timeCol = new TableColumn<>("Time");
        timeCol.setMaxWidth(1f * Integer.MAX_VALUE * 12);
        timeCol.setCellValueFactory(new PropertyValueFactory<Reminder, LocalTime>("time"));
        timeCol.setCellFactory(column -> cellFactories.getTableCellWithTimeFormatting());

        TableColumn<Reminder, Boolean> priorityCol = new TableColumn<>("Priority");
        priorityCol.setMaxWidth(1f * Integer.MAX_VALUE * 12);
        priorityCol.setCellValueFactory(new PropertyValueFactory<Reminder, Boolean>("priority"));
        priorityCol.setCellFactory(column -> {
            CheckBoxTableCell<Reminder, Boolean> cell = new CheckBoxTableCell<>();
            cell.setAlignment(Pos.CENTER);
            return cell;
        });

        TableColumn<Reminder, Boolean> completedCol = new TableColumn<>("Completed");
        completedCol.setMaxWidth(1f * Integer.MAX_VALUE * 15);
        completedCol.setCellValueFactory(new PropertyValueFactory<Reminder, Boolean>("completed"));
        completedCol.setCellFactory(column -> {
            CheckBoxTableCell<Reminder, Boolean> cell = new CheckBoxTableCell<>();
            cell.setAlignment(Pos.CENTER);
            return cell;
        });

        table.getColumns().addAll(nameCol, dateCol,timeCol, priorityCol, completedCol);
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        table.setPrefWidth(600);
        table.setRowFactory(tableView -> cellFactories.getTooltipTableRow());

        // Table row item double-click mouse event
        table.setOnMousePressed(mouseEvent -> {
            if ((mouseEvent.isPrimaryButtonDown()) &&
                    (mouseEvent.getClickCount() == 2)) {
                updateReminderRoutine();
            }
        });

        // Status message text
        actionStatus = new Text();
        actionStatus.setFill(Color.FIREBRICK);

        // Vbox with buttons hbox, table view and status text
        VBox vbox2 = new VBox(15);
        vbox2.getChildren().addAll(btnHb, table, actionStatus);

        // Hbox with vbox1 and vbox2
        hbPane = new HBox(15);
        hbPane.setPadding(new Insets(15));
        hbPane.setAlignment(Pos.CENTER);
        hbPane.getChildren().addAll(vbox1, vbox2);
    }

    /*
     * Gets all reminders from database and populates the table.
     * Initiates the reminder timer task.
     */
    @PostConstruct
    private void init() {

        try {
            dataAccess.loadRemindersFromDb();
        }
        catch(DataAccessException ex) {

            dataAccessException = ex;
            return;
        }

        table.setItems(dataAccess.getAllReminders());
        list.getSelectionModel().selectFirst();
        table.requestFocus();
        table.getSelectionModel().selectFirst();
		
        initiateReminderTimer();
        actionStatus.setText("Welcome to Reminders!");
    }

    private void initiateReminderTimer() {
	
        timer = new Timer();
        long zeroDelay = 0L;
        long period = 60000L; // 60 * 1000 = 1 min

        // The timer runs once (first time) for the overdue reminders
        // and subsequently the scheduled task every (one) minute
        timer.schedule(remindersTask, zeroDelay, period);
    }

    /*
     * The following three get methods are referred from the AppStarter class.
     */
    public DataAccessException getAppDatabaseException() {
	
        return dataAccessException;
    }
	
    public Parent getView() {
	
        return hbPane;
    }
	
    public Timer getTimer() {
	
        return timer;
    }

    /*
     * Reminder group list's item selection change listener class.
     * On selecting a group the group's reminders are shown in the table.
     * For example, selecting Priority shows only the reminders with
     * priority == true.
     */
    private class ListSelectChangeListener implements ChangeListener<Number> {

        @Override
        public void changed(ObservableValue<? extends Number> ov,
                Number oldVal, Number newVal) {

            int ix = newVal.intValue();

            if (ix >= 0) {

                String groupStr = list.getItems().get(ix);	
                ReminderGroup group = ReminderGroup.getGroup(groupStr);
                actionStatus.setText("");
                table.setItems(dataAccess.getTableDataForGroup(group));
                table.requestFocus();
                table.getSelectionModel().selectFirst();
            }
        }
    }

    /*
     * Routine for new reminder button action.
     * The new reminder is edited in the ReminderDialog
     * and is inserted in the table and the database.
     */
    private void newReminderRoutine() {

        LocalDate reminderDate = localDatePicker.getLocalDate();
        reminderDate =
            (reminderDate == null || reminderDate.isBefore(LocalDate.now()))
                ? LocalDate.now() : reminderDate;
        Dialog<Reminder> dialog = reminderDialog.create(reminderDate);	
        Optional<Reminder> result = dialog.showAndWait();
		
        if (result.isPresent()) {

            try {
                dataAccess.addReminder(result.get());
            }
            catch(DataAccessException ex) {
		
                String msg = "A database error occurred while inserting the new reminder, exiting the app.";
                AppStarter.showExceptionAlertAndExitTheApp(ex, msg);
            }

            refreshTable();
            actionStatus.setText("New reminder added.");
            table.getSelectionModel().selectLast();
        }
        else {
            table.requestFocus();
        }
    }

    /*
     * Refreshes the table with the updated rows after a
     * reminder is added, updated or deleted.
     */
    private void refreshTable() {
	
        table.setItems(dataAccess.getAllReminders());
        list.getSelectionModel().selectFirst();
        table.requestFocus();
    }

    /*
     * Routine for update reminder button action.
     * The selected reminder in the table is edited in the
     * ReminderDialog and is updated in the table and the database.
     */
    public void updateReminderRoutine() {
	
        Reminder rem = table.getSelectionModel().getSelectedItem();
		
        if ((table.getItems().isEmpty()) || (rem == null)) {
		
            return;
        }

        int ix = dataAccess.getAllReminders().indexOf(rem);
        Dialog<Reminder> dialog = reminderDialog.create(rem);
        Optional<Reminder> result = dialog.showAndWait();
		
        if (result.isPresent()) {

            try {
                dataAccess.updateReminder(ix, result.get());
            }
            catch(DataAccessException ex) {
		
                String msg = "A database error occurred while updating the reminder, exiting the app.";
                AppStarter.showExceptionAlertAndExitTheApp(ex, msg);
            }

            refreshTable();
            actionStatus.setText("Reminder updated.");
            table.getSelectionModel().select(ix);
        }
        else {
            table.requestFocus();
        }
    }

    /*
     * Routine for delete reminder button action.
     * Deletes the selected reminder from table and the database.
     */
    private void deleteReminderRoutine() {

        Reminder rem = table.getSelectionModel().getSelectedItem();
			
        if (table.getItems().isEmpty() || rem == null) {
			
            return;
        }

        Alert confirmAlert = getConfirmAlertForDelete(rem);
        Optional<ButtonType> result = confirmAlert.showAndWait();
			
        if ((result.isPresent()) && (result.get() == ButtonType.OK)) {

            try{
                dataAccess.deleteReminder(rem);
            }
            catch(DataAccessException ex) {
		
                String msg = "A database error occurred while deleting the reminder, exiting the app.";
                AppStarter.showExceptionAlertAndExitTheApp(ex, msg);
            }
			
            refreshTable();
            actionStatus.setText("Reminder deleted.");	
            table.getSelectionModel().selectFirst();
        }
        else {
            table.requestFocus();
        }
    }
		
    private Alert getConfirmAlertForDelete(Reminder rem) {

        Alert alert = new Alert(AlertType.CONFIRMATION);
        alert.setHeaderText(null);
        alert.setTitle("Reminder");
        alert.setContentText("Delete reminder? " + rem);
        return alert;
    }
}

Reminder.java

package com.javaquizplayer.examples.reminderapp;

import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;


/*
 * Class represents a reminder.
 *
 * NOTE: The reminder's time is captured as LocalTime which is truncated to 
 * the minutes.For example, 10:23:47.31987 is truncated to 10:23.
 * See the time attribute's setter method below.
 * Also, see the overridden equals method.
 */
public class Reminder {


    private SimpleStringProperty name;
    private SimpleStringProperty notes;
    private SimpleObjectProperty<LocalDate> date;
    private SimpleObjectProperty<LocalTime> time;
    private SimpleBooleanProperty priority;
    private SimpleBooleanProperty completed;


    public Reminder () {
    
        name = new SimpleStringProperty();
        notes = new SimpleStringProperty("");
        date = new SimpleObjectProperty<LocalDate>();
        time = new SimpleObjectProperty<LocalTime>();
        priority = new SimpleBooleanProperty();
        completed = new SimpleBooleanProperty(false);
    }

    public String getName() {
    
        return name.get();
    }
    public SimpleStringProperty nameProperty() {
    
        return name;
    }
    public void setName(String s) {
    
        name.set(s);
    }
    
    public String getNotes() {
    
        return notes.get();
    }
    public void setNotes(String s) {
    
        notes.set(s);
    }
    
    public LocalDate getDate() {
    
        return date.get();
    }
    public SimpleObjectProperty<LocalDate> dateProperty() {
    
        return date;
    }
    public void setDate(LocalDate d) {
    
        date.set(d);
    }
    
    public LocalTime getTime() {
    
        return time.get();
    }
    public SimpleObjectProperty<LocalTime> timeProperty() {
    
        return time;
    }
    public void setTime(LocalTime t) {
    
        // time is stored truncated to minutes
        time.set(t.truncatedTo(ChronoUnit.MINUTES));
    }
    
    public boolean getPriority() {
    
        return priority.get();
    }
    public SimpleBooleanProperty priorityProperty() {
    
        return priority;
    }
    public void setPriority(boolean b) {
    
        priority.set(b);
    }
    
    public boolean getCompleted() {
    
        return completed.get();
    }
    public SimpleBooleanProperty completedProperty() {
    
        return completed;
    }
    public void setCompleted(boolean b) {
    
        completed.set(b);
    }

    @Override
    public String toString() {
    
        return name.get();
    }
    
    /*
     * Reminders are equal when their names are same on the same date.
     */
    @Override
    public boolean equals(Object obj) {
    
        if (obj instanceof Reminder) {
        
            Reminder r = (Reminder) obj;
                
            if ((r.getName().equals(name.get())) &&
                    (r.getDate().isEqual(date.get()))) {
                    
                return true;
            }
        }
        
        return false;
    }
}

ReminderGroup.java

package com.javaquizplayer.examples.reminderapp;

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.EnumSet;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;


/*
 * Enum defines the reminder groups. This enum also stores the enum constant
 * formatted strings as a List<String> and a Map<String, ReminderGroup> for
 * populating a ListView and lookup respectively. The formatted string
 * for a constant is for example, "Completed" and COMPLETED respectively.
 */
public enum ReminderGroup {

    REMINDERS,
    TODAY,
    OVERDUE,
    COMPLETED,
    PRIORITY;

    /* Map with group formatted string as key and enum constant as value */
    private static final Map<String, ReminderGroup> map = new HashMap<>();
    
    /* List with group as formatted strings */
    private static final List<String> list = new ArrayList<>();

    static {
        for(ReminderGroup group : EnumSet.allOf(ReminderGroup.class)) {
                        
            String str = getEnumString(group);
            map.put(str, group);
            list.add(str);
        }
    }
    
    /*
     * Formats enum's string, for example: from REMINDERS to Reminders.
     */
    private static String getEnumString(ReminderGroup group) {

        String s = group.toString().toLowerCase();
        return (s.substring(0, 1).toUpperCase() + s.substring(1, s.length()));
    }

    public static ReminderGroup getGroup(String s) { 

        return map.get(s); 
    }
    
    public static ObservableList<String> getAsFormattedStrings() { 

        return FXCollections.observableList(list); 
    }
}

TableViewRowAndCellFactories.java

package com.javaquizplayer.examples.reminderapp;

import java.time.LocalDate;
import java.time.LocalTime;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableCell;
import javafx.scene.control.Tooltip;
import java.time.format.DateTimeFormatter;
import javafx.geometry.Pos;


/*
 * Class has methods to return row and cell factories to show
 * tooltip for each row and format date and time coulmns. These 
 * are applied the table view's and its respective columns in the app.
 */
public class TableViewRowAndCellFactories {

    /*
     * Returns TableCell with custom date formatting: dd.MMM.yyyy (10.MAR.2017)
     */
    public TableCell<Reminder, LocalDate> getTableCellWithDateFormatting() {

        TableCell<Reminder, LocalDate> cell = new TableCell<Reminder, LocalDate>() {
            
            @Override
            protected void updateItem(LocalDate date, boolean empty) {

                super.updateItem(date, empty);
                
                if (date == null || empty) {
            
                    setText(null);
                }
                else {
                    setText(date.format(DateTimeFormatter.ofPattern("dd.MMM.yyyy")));
                }
            }
        };
        
        cell.setAlignment(Pos.CENTER);
        return cell;
    }
    
    /*
     * Returns TableCell with custom time formatting: HH:mm (10:25)
     */
    public TableCell<Reminder, LocalTime> getTableCellWithTimeFormatting() {

        TableCell<Reminder, LocalTime> cell = new TableCell<Reminder, LocalTime>() {
            
            @Override
            protected void updateItem(LocalTime time, boolean empty) {

                super.updateItem(time, empty);
                
                if (time == null || empty) {
            
                    setText(null);
                }
                else {
                    setText(time.format(DateTimeFormatter.ofPattern("HH:mm")));
                }
            }
        };
        
        cell.setAlignment(Pos.CENTER);
        return cell;
    }

    /*
     * Returns TableRow with tooltip - reminder notes text (upto 100 chars).
     */
    public TableRow<Reminder> getTooltipTableRow() {
            
        TableRow<Reminder> row = new TableRow<Reminder>() {
        
            @Override
            public void updateItem(Reminder rem, boolean empty) {
            
                super.updateItem(rem, empty);
                
                if (empty) {
                    
                    setTooltip(null);
                }
                else {
                    String notes = rem.getNotes();
                    
                    if ((notes == null) || (notes.isEmpty())) {
                    
                        setTooltip(null);
                    }
                    else {
                        notes =
                            (notes.length() > 100) ?
                                notes.substring(0, 100) + "..." :
                                    notes;
                        setTooltip(new Tooltip(notes));
                    }
                }
            }
        };
        
        return row;
    }
}

ReminderDialog.java

package com.javaquizplayer.examples.reminderapp;

import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextField;
import javafx.scene.control.TextArea;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.Tooltip;
import javafx.scene.control.DatePicker;
import javafx.scene.control.DateCell;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.TextFormatter;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.geometry.Insets;

import jfxtras.scene.control.LocalTimePicker;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Optional;


/*
 * Class has methods to create dialogs to accept Reminder info.
 * There are two methods: for creating a new reminder and update an existing one.
 * The methods return respective dialog to the app. The reminder data is
 * validated before returning the reminder value to the application.
 */
public class ReminderDialog {

		
    private DataAccess dataAccess;
    private boolean newReminder; // indicates if a reminder is new or not
    
    /* Reminder name - maximum and minimum characters allowed. */
    private static final int NAME_MAX_CHARS = 25;
    private static final int NAME_MIN_CHARS = 5;
    
    /* Reminder notes - maximum characters allowed. */
    private static final int NOTES_MAX_CHARS = 200;


    public ReminderDialog(DataAccess dataAccess) {
    
        this.dataAccess = dataAccess;
    }

    /*
     * Returns a dialog for new Reminders. The method accepts the
     * selected date from the LocalDatePicker in the app.
     */
    public Dialog<Reminder> create(LocalDate reminderDate) {
    
        newReminder = true;

        Dialog<Reminder> dialog = getDialog("New Reminder");
        TextField textFld = getTextField("");
        CheckBox priorityCheckBox = getPriorityCheckbox(false);
        TextArea textArea = getTextArea("");
        DatePicker datePicker = getDatePicker(reminderDate);
        LocalTimePicker timePicker = getTimePicker(LocalTime.now());
        Optional<CheckBox> completedCheckBoxOpt = Optional.empty();

        HBox hb = getHBoxWithWidgets(textFld, priorityCheckBox);
        VBox vb = getVBoxWithWidgets(hb, textArea, datePicker, timePicker, completedCheckBoxOpt);
        dialog.getDialogPane().setContent(vb);

        ButtonType okButtonType = getDialogButtonType(dialog);
        String oldName = "";
        LocalDate oldDate = null;
        Button okButton = getOkButton(dialog, okButtonType, oldName, textFld, oldDate, datePicker, timePicker);
                
        setResultConverterOnDialog(dialog, textFld, textArea, datePicker, timePicker, priorityCheckBox, completedCheckBoxOpt, okButtonType);
        
        return dialog;
    }

    private Dialog<Reminder> getDialog(String title) {
    
        Dialog<Reminder> dialog = new Dialog<>();
        dialog.setTitle(title);
        dialog.setResizable(false);
        return dialog;
    }
        
    private TextField getTextField(String name) {

        TextField textFld = new TextField(name);
        textFld.setPrefColumnCount(20);
        textFld.setTooltip(new Tooltip("Reminder name must be " + NAME_MIN_CHARS + "  to " + NAME_MAX_CHARS + " characters"));
        textFld.setPromptText("Reminder name (" + NAME_MIN_CHARS + "  to " + NAME_MAX_CHARS + " chars)");
        return textFld;
    }
    
    private TextArea getTextArea(String text) {
        
        TextArea textArea = new TextArea(text);
        textArea.setPromptText("Notes upto " + NOTES_MAX_CHARS + " characters.");
        textArea.setTooltip(new Tooltip("Reminder notes upto " + NOTES_MAX_CHARS + " characters"));
        textArea.setPrefRowCount(3);
        textArea.setPrefColumnCount(20);
        textArea.setWrapText(true);
        
        // The text formatter limits the number of characters entered in
        // the notes text area to NOTES_MAX_CHARS.
        // The text formatter has a filter as constructor parameter (is of type
        // UnaryOperator<TextFormatter.Change>).
        textArea.setTextFormatter(new TextFormatter<String>(change ->
            change.getControlNewText().length() <= NOTES_MAX_CHARS ? change : null));
    
        return textArea;
    }
    
    private CheckBox getPriorityCheckbox(boolean isChecked) {
    
        CheckBox checkBox = new CheckBox("Priority");
        checkBox.setSelected(isChecked);
        return checkBox;
    }
    
    private CheckBox getCompletedCheckbox(boolean isChecked) {
    
        CheckBox checkBox = new CheckBox("Completed");
        checkBox.setSelected(isChecked);
        return checkBox;
    }
    
    private DatePicker getDatePicker(LocalDate date) {
    
        DatePicker datePicker = new DatePicker(date);
        datePicker.setTooltip(new Tooltip("Reminder date: click the icon to open calendar"));
        datePicker.setEditable(false);
        datePicker.setDayCellFactory(dtPicker -> {
            return getCellWithDateDisabledBeforeToday();
        });

        return datePicker;
    }
    
    private DateCell getCellWithDateDisabledBeforeToday() {
    
        DateCell cell = new DateCell() {
                
            @Override
            public void updateItem(LocalDate date, boolean empty) {
                    
                super.updateItem(date, empty);
 
                if ((! empty) && (date.isBefore(LocalDate.now()))) {
                        
                    setDisable(true);
                }
            }
        };
            
        return cell;
    }
    
    private LocalTimePicker getTimePicker(LocalTime time) {

        LocalTimePicker timePicker = new LocalTimePicker(time);
        timePicker.setTooltip(new Tooltip("Reminder time: slide knobs to set the hour and minutes"));
        return timePicker;
    }

    private HBox getHBoxWithWidgets(TextField textFld, CheckBox checkBox) {
        
        HBox hb = new HBox(15);
        hb.setAlignment(Pos.BASELINE_CENTER);
        hb.getChildren().addAll(textFld, checkBox);    
        return hb;
    }
    
    private VBox getVBoxWithWidgets(HBox hb, TextArea text, DatePicker datePicker, LocalTimePicker timePicker, Optional%lt;CheckBox> checkBoxOptional) {
    
        VBox vb = new VBox(15);
        vb.setPadding(new Insets(15));
                
        if (checkBoxOptional.isPresent()) {
        
            vb.getChildren().addAll(hb, text, datePicker, timePicker, checkBoxOptional.get());
        }
        else {
            // Completed check box doesn't exist in case of a new reminder
            vb.getChildren().addAll(hb, text, datePicker, timePicker);
        }
        
        return vb;
    }
    
    private ButtonType getDialogButtonType(Dialog dialog) {
    
        ButtonType okButtonType = new ButtonType("Okay", ButtonData.OK_DONE);
        dialog.getDialogPane().getButtonTypes().add(okButtonType);
        return okButtonType;
    }
    
    /*
     * Constructs the 'Okay' button for the dialog and registers an ActionEvent 
     * event filter with it. The event handler validates the input data.
     * In case the input data is not valid, the event is stopped from further
     * propagation (doesn't allow the click okay button action).
     */
    private Button getOkButton(Dialog dialog, ButtonType okButtonType, String oldName, TextField textFld, LocalDate oldDate, DatePicker datePicker, LocalTimePicker timePicker) {
    
        Button okButton = (Button) dialog.getDialogPane().lookupButton(okButtonType);
        
        okButton.addEventFilter(ActionEvent.ACTION, event -> {
            
            if (! validate(oldName, textFld.getText(), oldDate, datePicker.getValue(), timePicker.getLocalTime())) {
                
                showValidationAlertDialog();        
                event.consume();
            }
        });
        
        return okButton;
    }
    
    /*
     * Reminder validation in this dialog.
     * The name must be between NAME_MIN_CHARS and NAME_MAX_CHARS characters 
     * and unique for a date. Date and time must be in future for new reminders.
     */
    private boolean validate(String oldName, String name, LocalDate oldDate, LocalDate date, LocalTime time) {
    
        name = name.trim();
    
        if ((name.length() < NAME_MIN_CHARS) ||
                (name.length() > NAME_MAX_CHARS)) {
        
            return false;
        }
        
        if ((newReminder) &&
                (LocalDate.now().isEqual(date)) &&
                (LocalTime.now().isAfter(time))) {
                
            return false;
        }

        if ((! newReminder) &&
                (name.equals(oldName)) &&
                (date.isEqual(oldDate))) {
        
            return true;
        }
    
        for (Reminder r : dataAccess.getAllReminders()) {
        
            if ((r.getName().equals(name)) && (r.getDate().isEqual(date))) {
            
                return false;
            }
        }
        
        return true;
    }
    
    private void showValidationAlertDialog() {

        Alert alert = new Alert(AlertType.NONE);
        alert.setTitle("Reminder");
        alert.getDialogPane().getButtonTypes().add(ButtonType.OK);
        String s = "Name must be " + NAME_MIN_CHARS + "  to " + NAME_MAX_CHARS + " characters and should be unique within a date.";
        String s1 = (newReminder) ? " The date and time must be in future." : ""; 
        alert.setContentText(s + s1);
        alert.show();
    }
    
    private void setResultConverterOnDialog(Dialog dialog, TextField textFld, TextArea textArea, DatePicker datePicker, LocalTimePicker timePicker, CheckBox priority, Optional<CheckBox> completedOpt, ButtonType okButtonType) {
    
        // The result converter returns the entered Reminder instance in case
        // of 'Okay' button is clicked, or null in case the dialog is cancelled.
        // The reminder is built from the dialog's widget values after validation.
        // This reminder instance is returned by the dialog as an Optional in the app.
        
        dialog.setResultConverter(buttonType -> {

            if (buttonType == okButtonType) {

                Reminder rem = new Reminder();
                rem.setName(textFld.getText().trim());
                rem.setNotes(textArea.getText().trim());
                rem.setDate(datePicker.getValue());
                rem.setTime(timePicker.getLocalTime());
                rem.setPriority(priority.isSelected());
                
                if (completedOpt.isPresent()) {
        
                    rem.setCompleted(completedOpt.get().isSelected());
                }
                else {
                    // Completed checkbox doesn't exist in case of a new reminder
                    rem.setCompleted(false);
                }
                
                return rem;
            }

            return null;
        });
    }

    /*
     * Returns a dialog for updating reminders. The method accepts the
     * reminder being updated.
     */
    public Dialog<Reminder> create(Reminder rem) {
    
        newReminder = false;

        String oldName = rem.getName();
        LocalDate oldDate = rem.getDate();
        
        Dialog<Reminder> dialog = getDialog("Reminder");
        TextField textFld = getTextField(oldName);
        CheckBox priorityCheckBox = getPriorityCheckbox(rem.getPriority());
        TextArea textArea = getTextArea(rem.getNotes());
        DatePicker datePicker = getDatePicker(oldDate);
        LocalTimePicker timePicker = getTimePicker(rem.getTime());
        CheckBox completedCheckBox = getCompletedCheckbox(rem.getCompleted());
        Optional<CheckBox> completedCheckBoxOpt = Optional.of(completedCheckBox);
        
        HBox hb = getHBoxWithWidgets(textFld, priorityCheckBox);
        VBox vb = getVBoxWithWidgets(hb, textArea, datePicker, timePicker, completedCheckBoxOpt);    
        dialog.getDialogPane().setContent(vb);

        ButtonType okButtonType = getDialogButtonType(dialog);
        Button okButton = getOkButton(dialog, okButtonType, oldName, textFld, oldDate, datePicker, timePicker);
        
        setResultConverterOnDialog(dialog, textFld, textArea, datePicker, timePicker, priorityCheckBox, completedCheckBoxOpt, okButtonType);
        
        return dialog;
    }
}

CheckRemindersTask.java

package com.javaquizplayer.examples.reminderapp;

import java.time.LocalTime;
import java.time.LocalDate;
import java.time.format.FormatStyle;
import java.time.format.DateTimeFormatter;
import java.awt.Toolkit;
import java.awt.geom.Point2D;
import java.util.List;
import java.util.ArrayList;
import java.util.TimerTask;
import java.util.stream.Collectors;
import java.util.function.Predicate;

import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.stage.Screen;
import javafx.geometry.Rectangle2D;
import javafx.application.Platform;
import javafx.collections.ObservableList;


/*
 * Class extends TimerTask and overrides its run() method.
 * The class is the input task for the Timer which schedules the
 * tasks and is initiated from the app. The tasks:
 * (i) one task at the start of the app and (ii) subsequent tasks
 * at one minute interval thru the duration of the app.
 * The first task notifies all the overdue reminders. The later
 * tasks notify the due reminder at that minute.
 * The reminder notification is shown in an Alert.
 */
public class CheckRemindersTask extends TimerTask {


    private DataAccess dataAccess;
    
    /*
     * The predicate initially is set to this value, which is applied only once.
     * The second and subsequent execution of the timer task uses
     * ReminderPredicates.now. See the run() method.
     */
    private Predicate<Reminder> predicate = ReminderPredicates.OVER_DUE;
    
    private List<Point2D.Double> alertPositions;
    private int nextAlertPosIndex;


    public CheckRemindersTask(DataAccess dataAccess) {
    
        this.dataAccess = dataAccess;
        createAlertPositions();    
        nextAlertPosIndex = 0;
    }

    /*
     * When a reminder is due a notification is shown in an alert.
     * When there are multiple alerts at the same minute they are
     * positioned on the screen without overlapping each other.
     * The positions are pre-defined and stored in a collection.
     */    
    private void createAlertPositions() {

        Rectangle2D r2d = Screen.getPrimary().getVisualBounds();
        double x = r2d.getWidth() / 5;
        double y = r2d.getHeight() / 5;
        Point2D.Double position = null;
        alertPositions = new ArrayList<Point2D.Double>();
        
        for (int i = 0; i < 10; i++) {
        
            position = new Point2D.Double(x, y);
            alertPositions.add(position);
            x += 25; y += 25;
        }
    }

    /*
     * Filters all the reminders with supplied criteria (predicate)
     * and shows the reminder alerts in the app.
     */
    @Override
    public void run() {
    
        List<Reminder> dueRems = getDueRems();    
        showNotifications(dueRems);
        predicate = ReminderPredicates.DUE_NOW;
    }
    
    private List<Reminder> getDueRems() {
    
        ObservableList<Reminder> rems = dataAccess.getAllReminders();        
        return rems.stream()
                    .filter(predicate)
                    .collect(Collectors.toList());
    }
    
    /*
     * Show notifications;
     * muliple alerts at the same time at different positions.
     * The alerts are displayed at 3 second interval.
     */
    private void showNotifications(List<Reminder> remsNow) {
    
        for (Reminder r : remsNow) {
        
            Platform.runLater(() -> showReminderAlert(r));
             
            try {
                Thread.sleep(3000); // 3 seconds
            }
            catch (InterruptedException e) {
            }
        }
    }
    
    private void showReminderAlert(Reminder rem) {

        Alert alert = new Alert(AlertType.NONE); // a modal alert

        // Format alert content
        String name = rem.getName();        
        alert.setTitle("Reminder - " + name);
        alert.getDialogPane().getButtonTypes().add(ButtonType.OK);
        String content = name + " " +
            (rem.getPriority() ? "[Priority]" : "") + "\n" +
            rem.getDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)) +
            "  " +
            rem.getTime().format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT));
        alert.setContentText(content);
        
        // Get alert's position
        Point2D.Double p = getNextAlertPosition();        
        alert.setX(p.getX());
        alert.setY(p.getY());
        
        // Display alert
        alert.show();
        Toolkit.getDefaultToolkit().beep();
    }

    private Point2D.Double getNextAlertPosition() {
                        
        Point2D.Double p = alertPositions.get(nextAlertPosIndex);
        nextAlertPosIndex =
            (nextAlertPosIndex == 9) ? nextAlertPosIndex = 0 : ++nextAlertPosIndex;
        return p;
    }
}

ReminderPredicates.java

package com.javaquizplayer.examples.reminderapp;

import java.util.function.Predicate;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;


/*
 * Class has definitions of Predicates used in this app. These are defined as
 * public static members and are used in various classes.
 */
public class ReminderPredicates {


    public static final Predicate<Reminder> ALL = r -> true;
    public static final Predicate<Reminder> COMPLETED =
                                            r -> (r.getCompleted() == true);
    public static final Predicate<Reminder> PRIORITY =
                                            r -> (r.getPriority() == true);
    public static final Predicate<Reminder> TODAYS = r ->
                                            r.getDate().isEqual(LocalDate.now());

    private static final Predicate<Reminder> PAST_DAYS = r ->
                                            r.getDate().isBefore(LocalDate.now());
    private static final Predicate<Reminder> OVER_DUE_TODAYS_TIME = r ->
                                            r.getTime().isBefore(LocalTime.now());

    public static final Predicate<Reminder> OVER_DUE =
                                        COMPLETED.negate()
                                            .and(PAST_DAYS
                                                .or(TODAYS
                                                    .and(OVER_DUE_TODAYS_TIME)));

    private static final Predicate<Reminder> TIME_NOW = r ->
            r.getTime().equals(LocalTime.now().truncatedTo(ChronoUnit.MINUTES));

    public static final Predicate<Reminder> DUE_NOW =
            TODAYS.and(TIME_NOW).and(COMPLETED.negate());
}

DataAccess.java

package com.javaquizplayer.examples.reminderapp;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.function.Predicate;


/*
 * Class is a data accessor for the app. Interacts with the database access
 * program DatabaseJdbcAccess. Maintains the reminders data in a collection.
 * Has methods to get all, add, update and delete reminders used by the app.
 * Also, has a method to return reminders for a given ReminderGroup.
 */
public class DataAccess {


    private DatabaseJdbcAccess dbAccess;
    private ObservableList<Reminder> remData;
    

    public DataAccess(DatabaseJdbcAccess databaseJdbcAccess) {
    
        dbAccess = databaseJdbcAccess;
    }
    
    /*
     * This method retrieves all the reminders from the database at the start
     * of the application. The reminders are populated in the GUI and also
     * stored locally in this class as a collection.
     */
    public void loadRemindersFromDb() {

        remData = FXCollections.observableList(dbAccess.getAllReminders());
    }
    
    public ObservableList<Reminder> getAllReminders() {    
        
        return remData;
    }
    
    public void addReminder(Reminder rem) {
    
        remData.add(rem);
        dbAccess.addReminder(rem);
    }

    public void updateReminder(int ix, Reminder rem) {
    
        Reminder old = remData.get(ix);
        remData.set(ix, rem);
        dbAccess.updateReminder(old, rem);
    }
    
    public void deleteReminder(Reminder rem) {
    
        remData.remove(rem);
        dbAccess.deleteReminder(rem);
    }
    
    public ObservableList<Reminder> getTableDataForGroup(ReminderGroup group) {
    
        Predicate<Reminder> remPredicate = null;
    
        switch (group) {
        
            case PRIORITY:            
                remPredicate = ReminderPredicates.PRIORITY;
                break;
            
            case COMPLETED:            
                remPredicate = ReminderPredicates.COMPLETED;
                break;
                
            case TODAY:            
                remPredicate = ReminderPredicates.TODAYS;
                break;
                
            case OVERDUE:            
                remPredicate = ReminderPredicates.OVER_DUE;        
                break;
            
            default:            
                remPredicate = ReminderPredicates.ALL;
        }
        
        List<Reminder> result = remData.stream()
                                .filter(remPredicate)
                                .collect(Collectors.toList());
                            
        return FXCollections.observableList(result);
    }
}

AppConfig.java

package com.javaquizplayer.examples.reminderapp;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;


@Configuration
@Import(DatabaseJdbcConfig.class)
public class AppConfig {


    @Bean
    public AppGui appGui(DataAccess dataAccess, CheckRemindersTask checkRemindersTask, ReminderDialog reminderDialog) {

        return new AppGui(dataAccess, checkRemindersTask, reminderDialog);
    }

    @Bean
    public ReminderDialog reminderDialog(DataAccess dataAccess) {

        return new ReminderDialog(dataAccess);
    }
    
    @Bean
    public CheckRemindersTask checkRemindersTask(DataAccess dataAccess) {

        return new CheckRemindersTask(dataAccess);
    }
    
    @Bean
    public DataAccess dataAccess(DatabaseJdbcAccess databaseJdbcAccess) {

        return new DataAccess(databaseJdbcAccess);
    }
}

DatabaseJdbcConfig.java

package com.javaquizplayer.examples.reminderapp;

import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class DatabaseJdbcConfig {


    private static final String JDBC_DRIVER = "org.hsqldb.jdbc.JDBCDriver";
    
	/* ifexists=true connection property disallows creating a new database.*/
	private static final String CONNECTION_URL =
            "jdbc:hsqldb:file:db/remindersDB;ifexists=true;";


    @Bean
    public DataSource dataSource() {

        // SingleConnectionDataSource wraps a single JDBC Connection
        // which is not closed after use.
        SingleConnectionDataSource ds = new SingleConnectionDataSource();
        ds.setDriverClassName(JDBC_DRIVER);
        ds.setUrl(CONNECTION_URL);
        ds.setUsername(""); ds.setPassword("");
        return ds;
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {

        return new JdbcTemplate(dataSource);
    }
    
    @Bean
    public DatabaseJdbcAccess databaseJdbcAccess(JdbcTemplate jdbcTemplate) {

        return new DatabaseJdbcAccess(jdbcTemplate);
    }
}

DatabaseJdbcAccess.java

package com.javaquizplayer.examples.reminderapp;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Date;
import java.sql.Time;
import java.util.List;
import java.time.LocalDate;
import java.time.LocalTime;


/*
 * Class has methods to get all, add, update and delete reminders from the
 * database. Shutsdown the database.
 *
 * NOTE: The database date and time column types are compatible with java.sql.Date
 * and Time respectively. The app captures date and time as java.time.LocalDate
 * and LocalTime respectively. These are converted to and from using the methods
 * in java.sql.Date and Time (for example, Date.valueOf(LocalDate) and
 * Date.toLocalDate()).
 */
public class DatabaseJdbcAccess {


    private JdbcTemplate jdbcTemplate;
    
    private static final String GET_ALL_REMINDERS = "SELECT * FROM REMINDERS_TABLE";
    private static final String INSERT_REMINDER =
            "INSERT INTO REMINDERS_TABLE  VALUES (?, ?, ?, ?, ?, ?)";
    private static final String UPDATE_REMINDER =
            "UPDATE REMINDERS_TABLE SET name=?, notes=?, date=?, time=?, priority=?, completed=? WHERE name=? AND date=?";
    private static final String DELETE_REMINDER =
            "DELETE FROM REMINDERS_TABLE WHERE name=? AND date=?";


    public DatabaseJdbcAccess(JdbcTemplate template) {

        jdbcTemplate = template;        
    }

    public List<Reminder> getAllReminders() {
    
        return jdbcTemplate.query(GET_ALL_REMINDERS,
        
            new RowMapper<Reminder>() {
            
                @Override
                public Reminder mapRow(ResultSet rs, int rowNum)
                        throws SQLException {
                            
                    Reminder r = new Reminder();
                    r.setName(rs.getString("name"));
                    r.setNotes(rs.getString("notes"));
                    r.setDate(rs.getDate("date").toLocalDate());
                    r.setTime(rs.getTime("time").toLocalTime());
                    r.setPriority(rs.getBoolean("priority"));
                    r.setCompleted(rs.getBoolean("completed"));
                    return r;
                }
            }                
        );
    }
    
    public void addReminder(Reminder r) {
        
        jdbcTemplate.update(INSERT_REMINDER,
            r.getName(),
            r.getNotes(),
            Date.valueOf(r.getDate()),
            Time.valueOf(r.getTime()),
            r.getPriority(),
            r.getCompleted()
        );
    }
    
    public void updateReminder(Reminder old, Reminder r) {
    
        jdbcTemplate.update(UPDATE_REMINDER,
            r.getName(),
            r.getNotes(),
            Date.valueOf(r.getDate()),
            Time.valueOf(r.getTime()),
            r.getPriority(),
            r.getCompleted(),
            old.getName(),
            Date.valueOf(old.getDate())
        );
    }
    
    public void deleteReminder(Reminder r) {
    
        jdbcTemplate.update(DELETE_REMINDER,
            r.getName(),
            Date.valueOf(r.getDate())
        );
    }
	
    public void shutdownDatabase() {

        // SHUTDOWN is a HSQLDB command to close the database.
        jdbcTemplate.execute("SHUTDOWN");
    }
}

AppStarter.java

package com.javaquizplayer.examples.reminderapp;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.dao.DataAccessException;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;

import java.util.Timer;
import java.awt.Toolkit;


/*
 * The reminder app's starter program.
 * Loads the Spring's application context and launches the main GUI.
 */
public class AppStarter extends Application {


    private AbstractApplicationContext context;


    public static void main(String... args) {

        Application.launch(args);
    }
    
    /*
     * Loads the spring application context from the Java-based configuration.
     * Gets the main GUI and displays.
     * In case of an initial database exception, shows an alert and
     * terminates the app.
     */
    @Override
    public void start(Stage primaryStage) {

        context = new AnnotationConfigApplicationContext(AppConfig.class);
        AppGui appGui = context.getBean(AppGui.class);
        Parent mainView = appGui.getView();
        primaryStage.setScene(new Scene(mainView, 925, 450)); // w x h
        primaryStage.setResizable(false);
        primaryStage.setTitle("Reminders App");
        primaryStage.show();

        // Check if any database exception while loading initial data.
        // Exit the app in such case.

        DataAccessException ex = appGui.getAppDatabaseException();
        
        if (ex != null) {

            String msg = "There is an error while loading reminders from the database, exiting the app.";
            showExceptionAlertAndExitTheApp(ex, msg);
        }
    }
    
    /*
     * NOTE: this method is commonly acccessed from the AppGui class.
     */
    public static void showExceptionAlertAndExitTheApp(Exception ex, String msg) {
    
        System.out.println("#### Error Message: " + ex.getMessage());
        Toolkit.getDefaultToolkit().beep();
        
        Alert alert = new Alert(AlertType.ERROR);
        alert.setTitle("Reminders - Error");;
        alert.setContentText(msg);
        alert.showAndWait();
        
        Platform.exit();
    }

    /*
     * Shutsdown the database.
     * Terminates the timer discarding the scheduled tasks.
     * Closes this application context destroying all beans.
     */
    @Override
    public void stop() {
        
        context.getBean(DatabaseJdbcAccess.class).shutdownDatabase();
        Timer timer = context.getBean(AppGui.class).getTimer();
        
        if (timer != null) {
        
            timer.cancel();
        }
        
        context.close();
    }
}

Return to top


This page uses Java code formatting from http://hilite.me/.