TodoFX2

(Difference between revisions)
(Created page with 'By John Kostaras ---- This article is an evolution of the [http://wiki.netbeans.org/TodoFX TodoFX] application. We shall see how to: * add a graph * port database access to JP…')
 
(17 intermediate revisions not shown)
Line 2: Line 2:
----
----
 +
 +
==Introduction==
This article is an evolution of the [http://wiki.netbeans.org/TodoFX TodoFX] application. We shall see how to:
This article is an evolution of the [http://wiki.netbeans.org/TodoFX TodoFX] application. We shall see how to:
Line 8: Line 10:
* port database access to JPA
* port database access to JPA
-
Before we continue, download the ''TodoFX'' application from here.
+
Before we continue, please download the ''TodoFX'' application from here.
==Add a graph==
==Add a graph==
JavaFX has very good support for graphs. We shall extend our ''TodoFX'' application by creating a statistics graph to display due date distribution.
JavaFX has very good support for graphs. We shall extend our ''TodoFX'' application by creating a statistics graph to display due date distribution.
-
We'll use a Bar Chart containing a bar for each month to show has many tasks expire in that particular month.
+
We'll use a Bar Chart containing a bar for each month to show how many tasks expire in that particular month.
#Right-click on the <code>viewcontroller</code> package and select <code>New --> Other --> JavaFX --> Empty FXML</code> as shown below. Click '''Next'''.
#Right-click on the <code>viewcontroller</code> package and select <code>New --> Other --> JavaFX --> Empty FXML</code> as shown below. Click '''Next'''.
Line 128: Line 130:
             TaskStatisticsController controller = loader.getController();
             TaskStatisticsController controller = loader.getController();
             try {
             try {
-
                 controller.setTaskData(TaskManagerDB.getInstance().listAllTasks(true));
+
                 controller.setTaskData(TaskListWrapper.wrap(
 +
                  TaskManagerDB.getInstance().listAllTasks(true)));
             } catch (DatabaseException ex) {
             } catch (DatabaseException ex) {
                 Logger.getLogger(Main.class.getName())
                 Logger.getLogger(Main.class.getName())
Line 160: Line 163:
[[File:TodoFX-Fig19.png|"Figure 19 - Task Statistics"]]
[[File:TodoFX-Fig19.png|"Figure 19 - Task Statistics"]]
 +
 +
You may try other JavaFX charts:
 +
 +
[[File:JavaFX_Chat.png‎|"Figure 19a - JavaFX charts class hierarchy"]]
==Support for JPA==
==Support for JPA==
-
JDBC is not used anymore in modern applications, when there is a choice to use an Object-Relational Mapping (ORM) library like the standard Java Persistence API (JPA).
+
JDBC is not used anymore in modern applications, when there is a choice to use an Object-Relational Mapping (ORM) library like the standard Java Persistence API (JPA). NetBeans provides very good support for JPA.
 +
 
 +
1. Right-click on <code>todofx</code> package and select <code>New --> Other --> Persistence --> Entity Classes from Database...</code> and click '''Next'''
 +
 
 +
[[File:TodoFX-Fig20.png|"Figure 20 - Start the JPA wizard"]]
 +
 
 +
2. Select your Database Connection, select the <code>TODO</code> table from the list of ''Available Tables'' and click on '''Add''' to add it to the list of ''Selected Tables''. Click '''Next'''.
 +
 
 +
[[File:TodoFX-Fig21.png|"Figure 21 - Select the Database Tables"]]
 +
 
 +
3. Edit the class name from <code>Todo</code> to <code>Task</code>. Leave the other settings as in the figure and click on '''Next'''.
 +
 
 +
[[File:TodoFX-Fig22.png|"Figure 22 - Edit the Entity Classes"]]
 +
 
 +
4. Leave the default settings in the ''Mapping Options'' page and click '''Finish'''.
 +
 
 +
Your project should be similar to the one shown in the figure:
 +
 
 +
[[File:TodoFX-Fig23.png|"Figure 23 - TodoFX project with JPA support"]]
 +
 
 +
The wizard has created the <code>persistence.xml</code> file that contains the details about the connection with the database:
 +
 
 +
<code><pre>
 +
<?xml version="1.0" encoding="UTF-8"?>
 +
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
 +
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 +
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
 +
                    http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
 +
  <persistence-unit name="TodoFX-JPAPU" transaction-type="RESOURCE_LOCAL">
 +
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
 +
    <class>todofx.Task</class>
 +
    <properties>
 +
      <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:file:C:\Users\ikost\db\todo.script"/>
 +
      <property name="javax.persistence.jdbc.user" value="SA"/>
 +
      <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver"/>
 +
      <property name="javax.persistence.jdbc.password" value=""/>
 +
    </properties>
 +
  </persistence-unit>
 +
</persistence>
 +
</pre></code>
 +
 
 +
It has also created <code>todofx.Task</code> class which is our Entity class. If you had chosen <code>todofx.model</code> package instead of <code>todofx</code>, the wizard would have created a <code>Task_1.java</code> file in order not to override <code>Task.java</code>.
 +
 
 +
Copy the changes from <code>todofx.Task</code> to <code>todofx.model.Task</code>. You may also replace <code>todofx.model.Task</code> with <code>todofx.Task</code> but I 'd personally keep the raw data types (e.g. <code>int, boolean</code> etc.) instead of the generated (<code>Integer, Boolean</code>) ones.
 +
 
 +
Another issue is with <code>dueDate</code>. JPA 2.1 does not support Java 8, while these lines are being written, so a workaround is needed (which we discuss shortly).
 +
 
 +
The result should be something like:
 +
 
 +
<code><pre>
 +
package todofx.model;
 +
 
 +
import java.io.Serializable;
 +
import java.time.LocalDate;
 +
import javax.persistence.Basic;
 +
import javax.persistence.Column;
 +
import javax.persistence.Convert;
 +
import javax.persistence.Entity;
 +
import javax.persistence.GeneratedValue;
 +
import javax.persistence.GenerationType;
 +
import javax.persistence.Id;
 +
import javax.persistence.NamedQueries;
 +
import javax.persistence.NamedQuery;
 +
import javax.persistence.Table;
 +
import javax.xml.bind.annotation.XmlRootElement;
 +
 
 +
/**
 +
* Task
 +
*
 +
* @author ikost
 +
*/
 +
@Entity
 +
@Table(name = "TODO")
 +
@XmlRootElement
 +
@NamedQueries({
 +
    @NamedQuery(name = "Task.findAll", query = "SELECT t FROM Task t"),
 +
    @NamedQuery(name = "Task.findAllOrderByPriority",
 +
        query = "SELECT t FROM Task t ORDER BY t.priority"),
 +
    @NamedQuery(name = "Task.findAllOrderByDueDate",
 +
        query = "SELECT t FROM Task t ORDER BY t.dueDate"),   
 +
    @NamedQuery(name = "Task.findById",
 +
        query = "SELECT t FROM Task t WHERE t.id = :id"),
 +
    @NamedQuery(name = "Task.findByDescription",
 +
        query = "SELECT t FROM Task t WHERE t.description = :description"),
 +
    @NamedQuery(name = "Task.findByPriority",
 +
        query = "SELECT t FROM Task t WHERE t.priority = :priority"),
 +
    @NamedQuery(name = "Task.findByCompleted",
 +
        query = "SELECT t FROM Task t WHERE t.completed = :completed"),
 +
//@NamedQuery(name = "Task.findByDuedate",
 +
        query = "SELECT t FROM Task t WHERE t.duedate = :duedate"),
 +
    @NamedQuery(name = "Task.findByAlert",
 +
        query = "SELECT t FROM Task t WHERE t.alert = true"),
 +
//@NamedQuery(name = "Task.findByDaysbefore",
 +
        query = "SELECT t FROM Task t WHERE t.daysbefore = :daysbefore"),
 +
    @NamedQuery(name = "Task.findByObs",
 +
        query = "SELECT t FROM Task t WHERE t.obs = :obs")})
 +
public class Task implements Serializable {
 +
 
 +
    private static final long serialVersionUID = 1L;
 +
    @Id
 +
    @GeneratedValue(strategy = GenerationType.IDENTITY)
 +
    @Basic(optional = false)
 +
    @Column(name = "ID")
 +
    private int id;
 +
    @Column(name = "DESCRIPTION")
 +
    private String description;
 +
    @Column(name = "PRIORITY")
 +
    private int priority;
 +
    @Column(name = "DUEDATE")
 +
//    @Convert(converter = LocalDateConverter.class)
 +
    private LocalDate dueDate;
 +
    @Column(name = "ALERT")
 +
    private boolean alert;
 +
    @Column(name = "DAYSBEFORE")
 +
    private int daysBefore;
 +
    @Column(name = "OBS")
 +
    private String obs;
 +
    @Column(name = "COMPLETED")
 +
    private boolean completed;
 +
 
 +
    public Task() {
 +
        this.id = 0;
 +
    }
 +
 
 +
    public Task(int id) {
 +
        this.id = id;
 +
    }
 +
 
 +
    public boolean isLate() {
 +
        LocalDate dateDue = getDueDate();
 +
        return (dateDue == null) ? false : dateDue.compareTo(LocalDate.now()) < 0;
 +
    }
 +
 
 +
    public boolean hasAlert() {
 +
        LocalDate dateDue = getDueDate();
 +
        if (!getAlert() || dateDue == null) {
 +
            return false;
 +
        } else {
 +
            int dias = dueDate.getDayOfYear() - LocalDate.now().getDayOfYear();
 +
            return dias <= getDaysBefore();
 +
        }
 +
    }
 +
 
 +
    public boolean isCompleted() {
 +
        return completed;
 +
    }
 +
 
 +
    public void setCompleted(boolean completed) {
 +
        this.completed = completed;
 +
    }
 +
 
 +
    public String getDescription() {
 +
        return description;
 +
    }
 +
 
 +
    public void setDescription(String description) {
 +
        this.description = description;
 +
    }
 +
 
 +
    public int getPriority() {
 +
        return priority;
 +
    }
 +
 
 +
    public void setPriority(int priority) {
 +
        this.priority = priority;
 +
    }
 +
 
 +
    public LocalDate getDueDate() {
 +
        return dueDate;
 +
    }
 +
 
 +
    public void setDueDate(LocalDate dueDate) {
 +
        this.dueDate = dueDate;
 +
    }
 +
 
 +
    public boolean getAlert() {
 +
        return alert;
 +
    }
 +
 
 +
    public void setAlert(boolean alert) {
 +
        this.alert = alert;
 +
    }
 +
 
 +
    public int getDaysBefore() {
 +
        return daysBefore;
 +
    }
 +
 
 +
    public void setDaysBefore(int daysBefore) {
 +
        this.daysBefore = daysBefore;
 +
    }
 +
 
 +
    public String getObs() {
 +
        return obs;
 +
    }
 +
 
 +
    public void setObs(String obs) {
 +
        this.obs = obs;
 +
    }
 +
 
 +
    public int getId() {
 +
        return id;
 +
    }
 +
 
 +
    public void setId(int id) {
 +
        this.id = id;
 +
    }
 +
 
 +
    @Override
 +
    public int hashCode() {
 +
        int hash = 7;
 +
        hash = 59 * hash + this.id;
 +
        return hash;
 +
    }
 +
 
 +
    @Override
 +
    public boolean equals(Object obj) {
 +
        if (this == obj) {
 +
            return true;
 +
        }
 +
        if (obj == null) {
 +
            return false;
 +
        }
 +
        if (getClass() != obj.getClass()) {
 +
            return false;
 +
        }
 +
        final Task other = (Task) obj;
 +
        if (this.id != other.id) {
 +
            return false;
 +
        }
 +
        return true;
 +
    }
 +
 
 +
    @Override
 +
    public String toString() {
 +
        return "Task{" + "id=" + id + ", description=" + description
 +
                + ", priority=" + priority + ", dueDate=" + dueDate
 +
                + ", alert=" + alert + ", daysBefore=" + daysBefore
 +
                + ", obs=" + obs + ", completed=" + completed + '}';
 +
    }
 +
 
 +
}
 +
</pre></code>
 +
 
 +
''Note! Here you value our design decision not to 'pollute' our <code>Task</code> Value Object with JavaFX properties. Having both JavaFX properties and JPA annotations would make the POJO quite complex and most importantly would mix model with presentation details. If, however, you find a value in doing it, annotate each JavaFX property with <code>@Transient</code> to indicate to JPA not to persist these attributes.''
 +
 
 +
Check a number of important changes in the above code:
 +
 
 +
* Two new named queries have been added: "Task.findAllOrderByPriority" and "Task.findAllOrderByDueDate"
 +
* Named query "Task.findByAlert" has been changed to <code>query = "SELECT t FROM Task t WHERE t.alert = true")</code>
 +
* It is important that there is a parameterless constructor, and that <code>id</code> is initialised to 0 and not to -1, because the auto-increment will set the first <code>id</code> value to be 0 and EclipseLink has problems with zero keys (see note below).
 +
* In order to keep <code>dueDate</code> as of type <code>LocalDate</code>, we removed the annotation <code>@Temporal(TemporalType.DATE)</code>. But for this to work, we need a converter (in JPA 2.1; JPA 2.2 or 3.0 will provide support for <code>java.time</code> and the following hack will be obsolete):
 +
 
 +
<code><pre>
 +
package todofx.model;
 +
 
 +
import java.time.LocalDate;
 +
import java.sql.Date;
 +
import javax.persistence.AttributeConverter;
 +
import javax.persistence.Converter;
 +
 
 +
@Converter(autoApply = true)
 +
public class LocalDateConverter implements AttributeConverter<LocalDate, Date> {
 +
 
 +
    @Override
 +
    public Date convertToDatabaseColumn(LocalDate locDate) {
 +
        return locDate == null ? null : Date.valueOf(locDate);
 +
    }
 +
 
 +
    @Override
 +
    public LocalDate convertToEntityAttribute(Date sqlDate) {
 +
        return sqlDate == null ? null : sqlDate.toLocalDate();
 +
    }
 +
}
 +
</pre></code>
 +
 
 +
to be defined in <code>persistence.xml</code>, too:
 +
 
 +
<code><pre>
 +
<?xml version="1.0" encoding="UTF-8"?>
 +
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
 +
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 +
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
 +
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
 +
  <persistence-unit name="TodoFXPU" transaction-type="RESOURCE_LOCAL">
 +
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
 +
    <class>todofx.model.Task</class>
 +
    <class>todofx.model.LocalDateConverter</class>
 +
    <properties>
 +
      <property name="javax.persistence.jdbc.url"
 +
              value="jdbc:hsqldb:file:C:\Users\ikost\db\todo.script"/>
 +
      <property name="javax.persistence.jdbc.user" value="SA"/>
 +
      <property name="javax.persistence.jdbc.driver"
 +
              value="org.hsqldb.jdbcDriver"/>
 +
      <property name="javax.persistence.jdbc.password" value=""/>
 +
    </properties>
 +
  </persistence-unit>
 +
</persistence>
 +
</pre></code>
 +
 
 +
This converter will apply the conversion <code>java.sql.Date <=> java.time.LocalDate</code> everywhere. If you don't want this, set <code>@Converter(autoApply = false)</code> and in the attributes where you want to apply the conversion, add <code>@Convert(converter = LocalDateConverter.class)</code>.
 +
 
 +
''Note! Beware of having a zero <code>id</code> value with EclipseLink (see this [http://stackoverflow.com/questions/9708149/how-to-start-from-0-an-unsigned-auto-increment-field discussion]). If you or TodoFX accidentally inserted a row in your database with <code>id=0</code>, simply connect to the database and change the 0 to another value or delete the row (see the original [http://www.oracle.com/technetwork/articles/java/rcp-todo-2194057.html article] on how to do this). Otherwise you will get weird errors while you are trying to connect. See the above discussion for other solutions.''
 +
 
 +
Now it is time to clean up our project and add our JPA-powered <code>TaskManager</code>. Get rid off <code>DatabaseException.java, ModelException.java, Parameters.java, TaskManager.java, TaskManagerDB.java</code> and <code>todofx.Task.java</code>.
 +
 
 +
Add the following class:
 +
<code><pre>
 +
package todofx.model;
 +
 
 +
import java.util.List;
 +
import java.util.Optional;
 +
import javax.persistence.EntityManager;
 +
import javax.persistence.Persistence;
 +
 
 +
/**
 +
*
 +
* @author ikost
 +
*/
 +
public class TaskManager {
 +
 
 +
    private final EntityManager entityManager;
 +
 
 +
    private TaskManager() {
 +
        entityManager = Persistence.createEntityManagerFactory("TodoFXPU").createEntityManager();
 +
    }
 +
 
 +
    public String getDatabaseUrl() {
 +
        String dbName = "";
 +
        String url = entityManager.getProperties().get("javax.persistence.jdbc.url").toString();
 +
        if (url != null) {
 +
            dbName = url.substring(url.lastIndexOf("/") + 1);
 +
        }
 +
        return dbName;
 +
    }
 +
 
 +
    private final static class SingletonHolder {
 +
 
 +
        private final static TaskManager INSTANCE = new TaskManager();
 +
    }
 +
 
 +
    public static TaskManager getInstance() {
 +
        return SingletonHolder.INSTANCE;
 +
    }
 +
 
 +
    public List<Task> listAllTasks(final boolean priorityOrDueDate) {
 +
        String param = priorityOrDueDate ?
 +
              "Task.findAllOrderByPriority" : "Task.findAllOrderByDueDate";
 +
        return entityManager.createNamedQuery(param).getResultList();
 +
    }
 +
 
 +
    public List<Task> listTasksWithAlert() {
 +
        return entityManager.createNamedQuery("Task.findByAlert").getResultList();
 +
    }
 +
 
 +
    public void addTask(final Task task) throws ValidationException {
 +
        validate(task);
 +
        entityManager.getTransaction().begin();
 +
        try {
 +
            entityManager.persist(task);
 +
            entityManager.getTransaction().commit();
 +
        } catch (Exception e) {
 +
            entityManager.getTransaction().rollback();
 +
        } finally {
 +
        }
 +
    }
 +
 
 +
    public void updateTask(final Task task) throws ValidationException {
 +
        validate(task);
 +
        Task oldTask = findTask(task.getId()).get();
 +
        if (oldTask != null) {
 +
            entityManager.getTransaction().begin();
 +
            try {
 +
                entityManager.persist(task);
 +
                entityManager.getTransaction().commit();
 +
            } catch (Exception e) {
 +
                entityManager.getTransaction().rollback();
 +
            } finally {
 +
            }
 +
        }
 +
    }
 +
 
 +
    public void markAsCompleted(final int id, final boolean completed) {
 +
        Task task = findTask(id).get();
 +
        if (task != null) {
 +
            task.setCompleted(completed);
 +
            entityManager.getTransaction().begin();
 +
            try {
 +
                entityManager.persist(task);
 +
                entityManager.getTransaction().commit();
 +
            } catch (Exception e) {
 +
                entityManager.getTransaction().rollback();
 +
            } finally {
 +
            }
 +
        }
 +
    }
 +
 
 +
    public void removeTask(final Task task) {
 +
        entityManager.getTransaction().begin();
 +
        try {
 +
            entityManager.remove(task);
 +
            entityManager.getTransaction().commit();
 +
        } catch (Exception e) {
 +
            entityManager.getTransaction().rollback();
 +
        } finally {
 +
        }
 +
    }
 +
 
 +
    private boolean isEmpty(final String str) {
 +
        return str == null || str.trim().isEmpty();
 +
    }
 +
 
 +
    private void validate(final Task task) throws ValidationException {
 +
        if (isEmpty(task.getDescription())) {
 +
            throw new ValidationException("Must provide a task description");
 +
        }
 +
    }
 +
 
 +
    private Optional<Task> findTask(final int id) {
 +
        return Optional.of((Task) entityManager.find(Task.class, id));
 +
    }
 +
   
 +
}
 +
</pre></code>
 +
 
 +
The only missing functionality is that of the <code>reconnect</code> method which we 'll fix later.
 +
 
 +
Let's fix our compilation errors. In <code>TaskDetailsDialogController.handleRemove()</code>, change the line with <code>TaskManagerDB</code> to:
 +
 
 +
<code><pre>
 +
TaskManager.getInstance().removeTask(task.getTask());
 +
</pre></code>
 +
 
 +
Similarly <code>TaskMainController</code> has been changed to:
 +
 
 +
<code><pre>
 +
package todofx.viewcontroller;
 +
 
 +
import java.io.File;
 +
import java.net.URL;
 +
import java.time.LocalDate;
 +
import java.util.ArrayList;
 +
import java.util.List;
 +
import java.util.Optional;
 +
import java.util.ResourceBundle;
 +
import java.util.logging.Level;
 +
import java.util.logging.Logger;
 +
import javafx.collections.ObservableList;
 +
import javafx.fxml.FXML;
 +
import javafx.fxml.Initializable;
 +
import javafx.scene.control.Alert;
 +
import javafx.scene.control.Button;
 +
import javafx.scene.control.ButtonType;
 +
import javafx.scene.control.CheckMenuItem;
 +
import javafx.scene.control.Label;
 +
import javafx.scene.control.SelectionMode;
 +
import javafx.scene.control.TableCell;
 +
import javafx.scene.control.TableColumn;
 +
import javafx.scene.control.TableRow;
 +
import javafx.scene.control.TableView;
 +
import javafx.scene.control.ToggleButton;
 +
import javafx.scene.control.ToggleGroup;
 +
import javafx.scene.paint.Color;
 +
import javafx.stage.FileChooser;
 +
import javafx.stage.FileChooser.ExtensionFilter;
 +
import todofx.Main;
 +
import todofx.model.Task;
 +
import todofx.model.TaskManager;
 +
import todofx.util.DateUtil;
 +
 
 +
/**
 +
*
 +
* @author ikost
 +
*/
 +
public class TaskMainController implements Initializable {
 +
 
 +
    @FXML
 +
    private TableView<TaskWrapper> taskTable;
 +
    @FXML
 +
    private TableColumn<TaskWrapper, Integer> priorityColumn;
 +
    @FXML
 +
    private TableColumn<TaskWrapper, String> descriptionColumn;
 +
    @FXML
 +
    private TableColumn<TaskWrapper, Boolean> alertColumn;
 +
    @FXML
 +
    private TableColumn<TaskWrapper, LocalDate> dueDateColumn;
 +
    @FXML
 +
    private Label lblMessage;
 +
    @FXML
 +
    private ToggleButton chkShowCompletedTasks;
 +
    @FXML
 +
    private CheckMenuItem chkMnuShowCompletedTasks;
 +
    @FXML
 +
    private ToggleButton chkSortBy;
 +
    @FXML
 +
    private Button btnMarkAsCompleted;
 +
    @FXML
 +
    private ToggleGroup toggleGroup;
 +
 
 +
    private Main mainApp;
 +
 
 +
    @Override
 +
    public void initialize(URL url, ResourceBundle rb) {
 +
        // Add observable list data to the table
 +
        taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
 +
        taskTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
 +
        taskTable.setOnMousePressed(event -> {
 +
            if (event.isPrimaryButtonDown() && event.getClickCount() == 2
 +
                    && taskTable.getSelectionModel().getSelectedIndex() > -1) {
 +
                handleEditTask();
 +
            }
 +
        });
 +
        // Initialize the table
 +
        priorityColumn.setCellValueFactory(cellData ->
 +
            cellData.getValue().getPriorityProperty().asObject());
 +
        descriptionColumn.setCellValueFactory(cellData ->
 +
            cellData.getValue().getDescriptionProperty());
 +
        alertColumn.setCellValueFactory(cellData ->
 +
            cellData.getValue().getAlertProperty());
 +
        dueDateColumn.setCellValueFactory(cellData ->
 +
            cellData.getValue().getDueDateProperty());
 +
        dueDateColumn.setCellFactory(column -> {
 +
            return new TableCell<TaskWrapper, LocalDate>() {
 +
                @Override
 +
                protected void updateItem(LocalDate item, boolean empty) {
 +
                    super.updateItem(item, empty);
 +
                    if (item == null || empty) {
 +
                        setText(null);
 +
                        setStyle("");
 +
                    } else {
 +
                        setText(DateUtil.format(item));
 +
                    }
 +
                }
 +
            };
 +
        });
 +
        final String defaultStyle = taskTable.getStyle();
 +
        taskTable.setRowFactory(tableView -> {
 +
            return new TableRow<TaskWrapper>() {
 +
                @Override
 +
                protected void updateItem(TaskWrapper item, boolean empty) {
 +
                    super.updateItem(item, empty);
 +
                    if (!empty) {
 +
                        if (item.isCompleted()) {
 +
                            setStyle("-fx-background-color: #95caff;");
 +
                        } else if (item.isLate()) {
 +
                            setStyle("-fx-background-color: #f06f06;");
 +
                        } else if (item.hasAlert()) {
 +
                            setStyle("-fx-background-color: #ffff00;");
 +
                        } else {
 +
                            setStyle(defaultStyle);
 +
                        }
 +
                    } else {
 +
                        setStyle(defaultStyle);
 +
                    }
 +
                }
 +
            };
 +
        });
 +
        handleShowAlerts();
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks on New Task List.
 +
    */
 +
    @FXML
 +
    private void handleNewTaskList() {
 +
        FileChooser dlg = new FileChooser();
 +
        dlg.setTitle("New Task List");
 +
        dlg.getExtensionFilters().addAll(
 +
                new ExtensionFilter("Task Lists - HSQLDB Databases (*.script)", "*.script"),
 +
                new ExtensionFilter("All Files", "*.*")
 +
        );
 +
        File dir = new File(TaskManager.getInstance().getDatabaseUrl()).getParentFile();
 +
        dlg.setInitialDirectory(dir);
 +
        File selectedFile = dlg.showSaveDialog(mainApp.getPrimaryStage());
 +
        if (selectedFile != null) {
 +
            try {
 +
                String arq = createOpenDatabase(selectedFile);
 +
                setStatus("Task list created: " + arq, false);
 +
            } catch (Exception e) {
 +
                setStatus("Cannot create task list", true);
 +
            }
 +
        }
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks on Open Task List.
 +
    */
 +
    @FXML
 +
    private void handleOpenTaskList() {
 +
        FileChooser dlg = new FileChooser();
 +
        dlg.setTitle("Open Task List");
 +
        dlg.getExtensionFilters().addAll(
 +
                new ExtensionFilter("Task Lists - HSQLDB Databases (*.script)", "*.script"),
 +
                new ExtensionFilter("All Files", "*.*")
 +
        );
 +
        File dir = new File(TaskManager.getInstance().getDatabaseUrl()).getParentFile();
 +
        dlg.setInitialDirectory(dir);
 +
        File selectedFile = dlg.showOpenDialog(mainApp.getPrimaryStage());
 +
        if (selectedFile != null) {
 +
            try {
 +
                String arq = createOpenDatabase(selectedFile);
 +
                setStatus("Task List opened: " + arq, false);
 +
            } catch (Exception e) {
 +
                setStatus("Cannot open task list", true);
 +
            }
 +
        }
 +
    }
 +
 
 +
    private String createOpenDatabase(File databaseFile) {
 +
        String fileName = databaseFile.getAbsolutePath();
 +
        if (fileName.startsWith("file:")) {
 +
            fileName = fileName.substring(5);
 +
        }
 +
        if (fileName.endsWith(".script")) {
 +
            fileName = fileName.substring(0, fileName.length() - 7);
 +
        }
 +
//        TaskManager.getInstance().reconnect(fileName);
 +
        taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
 +
        return fileName;
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Show Alerts.
 +
    */
 +
    @FXML
 +
    private void handleShowAlerts() {
 +
        List<Task> tasksWithAlert = new ArrayList<>();
 +
        tasksWithAlert = TaskManager.getInstance().listTasksWithAlert();
 +
        tasksWithAlert.stream().forEach(t -> {
 +
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
 +
            alert.setTitle("Alert");
 +
            alert.setHeaderText("Task: " + t.getDescription());
 +
            alert.setContentText("expired on " + DateUtil.format(t.getDueDate()));
 +
            alert.showAndWait();
 +
        });
 +
        if (tasksWithAlert.isEmpty()) {
 +
            setStatus("There are no tasks with alerts for today.", false);
 +
        } else {
 +
            setStatus("There are " + tasksWithAlert.size() + " task(s) with alerts for today.", false);
 +
        }
 +
    }
 +
 
 +
    @FXML
 +
    private void handleAbout() {
 +
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
 +
        alert.setTitle("About TodoFX");
 +
        alert.setHeaderText("TodoFX - Task List\nRelease 1.0\n\n"
 +
                + "                http://wiki.netbeans.org/TodoFX");
 +
        alert.setContentText("Author: Ioannis Kostaras");
 +
        alert.showAndWait();
 +
    }
 +
 
 +
    @FXML
 +
    private void handleNewTask() {
 +
        Task tempTask = new Task();
 +
        boolean saveClicked = mainApp.showTaskDetailsDialog(new TaskWrapper(tempTask));
 +
        if (saveClicked) {
 +
            try {
 +
                TaskManager.getInstance().addTask(tempTask);
 +
            } catch (ValidationException ex) {
 +
                Logger.getLogger(TaskMainController.class.getName()).log(Level.SEVERE, null, ex);
 +
            }
 +
            // Add observable list data to the table
 +
            taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
 +
        }
 +
    }
 +
 
 +
    @FXML
 +
    private void handleEditTask() {
 +
        TaskWrapper selectedTask = taskTable.getSelectionModel().getSelectedItem();
 +
        if (selectedTask != null) {
 +
            boolean saveClicked = mainApp.showTaskDetailsDialog(selectedTask);
 +
            if (saveClicked) {
 +
                try {
 +
                    TaskManager.getInstance().updateTask(selectedTask.getTask());
 +
                } catch (ValidationException ex) {
 +
                    Logger.getLogger(TaskMainController.class.getName()).log(Level.SEVERE, null, ex);
 +
                }
 +
                taskTable.refresh();
 +
            }
 +
        } else {
 +
            // Nothing selected.
 +
            Alert alert = new Alert(Alert.AlertType.WARNING);
 +
            alert.setTitle("No Selection");
 +
            alert.setHeaderText("No Task Selected");
 +
            alert.setContentText("Please select a task in the table.");
 +
 
 +
            alert.showAndWait();
 +
        }
 +
    }
 +
 
 +
    @FXML
 +
    private void handleRemoveTask() {
 +
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
 +
        selectedTasks.stream().forEach(task -> {
 +
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
 +
            alert.setTitle("Remove Task");
 +
            alert.setHeaderText("Are you sure you want to remove task? ");
 +
            alert.setContentText(task.getDescription());
 +
            Optional<ButtonType> result = alert.showAndWait();
 +
            if (result.get() == ButtonType.OK) {
 +
                taskTable.getItems().remove(task);
 +
                TaskManager.getInstance().removeTask(task.getTask());
 +
                taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
 +
            }
 +
        });
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Show Completed Tasks.
 +
    */
 +
    private void showCompletedTasks(boolean show) {
 +
        if (show) {
 +
            taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true))
 +
                    .filtered(t -> t.isCompleted()));
 +
        } else {
 +
            taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
 +
        }
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks on Show Completed Tasks check button.
 +
    */
 +
    @FXML
 +
    private void handleButtonShowCompletedTasks() {
 +
        showCompletedTasks(chkShowCompletedTasks.isSelected());
 +
        chkMnuShowCompletedTasks.setSelected(chkShowCompletedTasks.isSelected());
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Show Completed Tasks check menu item.
 +
    */
 +
    @FXML
 +
    private void handleMenuItemShowCompletedTasks() {
 +
        showCompletedTasks(chkMnuShowCompletedTasks.isSelected());
 +
        chkShowCompletedTasks.setSelected(chkMnuShowCompletedTasks.isSelected());
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Sort By Priority/Due Date check button.
 +
    */
 +
    @FXML
 +
    private void handleSortBy() {
 +
        sortBy(chkSortBy.isSelected());
 +
    }
 +
 
 +
    /**
 +
    * @param byPriority if {@code true} sort tasks by priority, otherwise by
 +
    * due date
 +
    */
 +
    private void sortBy(boolean byPriority) {
 +
        taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(byPriority)));
 +
        chkSortBy.setSelected(byPriority);
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Sort By Priority menu item.
 +
    */
 +
    @FXML
 +
    private void handleSortByPriority() {
 +
        sortBy(true);
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Sort By Due Date menu item.
 +
    */
 +
    @FXML
 +
    private void handleSortByDueDate() {
 +
        sortBy(false);
 +
    }
 +
 
 +
    /**
 +
    * Called when the user clicks Mark As Completed.
 +
    */
 +
    @FXML
 +
    private void handleMarkAsCompleted() {
 +
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
 +
        selectedTasks.forEach(selectedTask -> {
 +
            if (selectedTask != null) {
 +
                selectedTask.setCompleted(true);
 +
                TaskManager.getInstance().markAsCompleted(selectedTask.getId(), true);
 +
            } else {
 +
                Alert alert = new Alert(Alert.AlertType.WARNING);
 +
                alert.setTitle("Warning");
 +
                alert.setHeaderText("No Task Selected");
 +
                alert.setContentText("Select a task first");
 +
                alert.showAndWait();
 +
            }
 +
        });
 +
        taskTable.refresh();
 +
    }
 +
 
 +
    /**
 +
    * Opens the task statistics.
 +
    */
 +
    @FXML
 +
    private void handleShowTaskStatistics() {
 +
        mainApp.showTaskStatistics();
 +
    }
 +
 
 +
    /**
 +
    * Closes the application.
 +
    */
 +
    @FXML
 +
    private void handleExit() {
 +
        System.exit(0);
 +
    }
 +
 
 +
    public void setMainApp(Main mainApp) {
 +
        this.mainApp = mainApp;
 +
    }
 +
 
 +
    public void setStatus(String msg, boolean isError) {
 +
        lblMessage.setText(msg);
 +
        if (isError) {
 +
            lblMessage.setTextFill(Color.RED);
 +
        } else {
 +
            lblMessage.setTextFill(Color.BLACK);
 +
        }
 +
    }
 +
}
 +
</pre></code>
 +
 
 +
Clean and build and run your application.
 +
 
 +
''Note! When you clean and build the project, you might encounter an exception <code>class file for sun.util.logging.PlatformLogger not found</code>. Fortunately, there is an easy [http://stackoverflow.com/questions/22562797/javafx8-sun-util-logging-platformlogger-not-found-exception-in-netbeans-8 solution]:
 +
 
 +
# Right Click on the project and select "Properties".
 +
# Select "Libraries" from opened "Project Properties window".
 +
# Select "Processor" tab.
 +
# Remove EclipseLink from there.
 +
# Click on '''OK''' and do a Clean Build.''
 +
 
 +
The application should behave as before, apart from the two menu items <code>File --> New task list</code> and <code>File --> Open task list</code>.
 +
 
 +
The program works because you have an already created <code>db/todo</code> database. But if you don't then it will fail. We need to create an empty database schema, if there isn't one.
 +
 
 +
JPA 2.1 added, finally, support for creating the database schema. According to references [11-13] and this [https://docs.oracle.com/javaee/7/tutorial/persistence-intro005.htm tutorial], you need to add these properties to your <code>persistence.xml</code>
 +
 
 +
<code><pre>
 +
      <property name="javax.persistence.schema-generation.database.action" value="create"/>
 +
      <property name="javax.persistence.schema-generation.scripts.action" value="create"/>
 +
      <property name="javax.persistence.schema-generation.scripts.create-target" value="resources/sql/createDB.ddl"/>
 +
      <property name="javax.persistence.schema-generation.scripts.drop-target" value="resources/sql/dropDB.ddl"/>
 +
      <!--<property name="javax.persistence.sql-load-script-source" value="resources/sql/insert.sql"/>--> 
 +
</pre></code>
 +
 
 +
and create the following <code>.sql</code> scripts inside <code>resources/sql</code> folder:
 +
 
 +
<code><pre>
 +
//createDB.sql
 +
CREATE TABLE PUBLIC.todo (
 +
    id IDENTITY,
 +
    description VARCHAR(100),
 +
    priority INTEGER,
 +
    completed BOOLEAN,
 +
    dueDate DATE,
 +
    alert BOOLEAN,
 +
    daysBefore INTEGER,
 +
    obs VARCHAR(250)
 +
)
 +
 
 +
//dropDB.sql
 +
DROP TABLE PUBLIC.todo;
 +
</pre></code>
 +
and an optional <code>insert.sql</code> with <code>INSERT</code> SQL statements to insert sample data on table creation (here we have commented it out). To create an empty SQL file with NetBeans, click on '''File --> New --> Other --> SQL file'''.
 +
 
 +
The final step is to add the following line to the beginning of the constructor of <code>TaskManager</code>:
 +
 
 +
<code><pre>
 +
Persistence.generateSchema("TodoFXPU", null);
 +
</pre></code>
 +
 
 +
Now, if the <code>db/todo</code> database does not exist, it will be created the first time the program is executed. Don't forget to clean and build for the changes in <code>persistence.xml</code> to take effect.
 +
 
 +
But we still haven't created a <code>reconnect()</code> method. Add the following to <code>TaskManager</code>:
 +
 
 +
<code><pre>
 +
public class TaskManager {
 +
 
 +
    private EntityManager entityManager;
 +
    private final Map<String, Object> properties;
 +
    private static final String DEFAULT_URL = "jdbc:hsqldb:file:";
 +
 
 +
    private TaskManager() {
 +
        Persistence.generateSchema("TodoFXPU", null);
 +
        entityManager = Persistence.createEntityManagerFactory("TodoFXPU").createEntityManager();
 +
        properties = new HashMap<>(entityManager.getProperties());
 +
    }
 +
 
 +
    public void reconnect(String database) {
 +
        entityManager.close();
 +
        properties.put("javax.persistence.jdbc.url", DEFAULT_URL + database);
 +
        Persistence.generateSchema("TodoFXPU", properties);
 +
        entityManager = Persistence.createEntityManagerFactory("TodoFXPU", properties).createEntityManager();
 +
    }   
 +
 
 +
    ...
 +
</pre></code>
 +
 
 +
The values of the <code>properties</code> hash map override any ''properties'' read by <code>persistence.xml</code>.
 +
 
 +
Finally, in <code>TaskMainController</code>, method <code>createOpenDatabase()</code> uncomment the <code>reconnect()</code> method:
 +
 
 +
<code>
 +
    private String createOpenDatabase(File databaseFile) {
 +
        String fileName = databaseFile.getAbsolutePath();
 +
        if (fileName.startsWith("file:")) {
 +
            fileName = fileName.substring(5);
 +
        }
 +
        if (fileName.endsWith(".script")) {
 +
            fileName = fileName.substring(0, fileName.length() - 7);
 +
        }
 +
        TaskManager.getInstance().reconnect(fileName);
 +
        taskTable.setItems(TaskListWrapper.wrap(
 +
                  TaskManager.getInstance().listAllTasks(true)));
 +
        return fileName;
 +
    }
 +
</code>
 +
 
 +
Rebuild and test the application to see that it behaves like the initial Swing <code>Todo</code> one.
 +
 
 +
==Recap==
 +
In this tutorial we show how we can port a Java Swing standalone application to JavaFX using the NetBeans IDE. We also show how to add graphs and how to port JDBC to JPA. We tried to keep a good design following the Model-View-Controller (MVC) design pattern.
 +
 
 +
You may compare it to the original [http://netbeans.org/download/magazine/01/nb01_completeapp.pdf article] as well as to the [http://www.oracle.com/technetwork/articles/java/rcp-todo-2194057.html TodoRCP] version.
==References==
==References==
Line 173: Line 1,104:
#Tamam M. (2015), ''JavaFX Essentials'', APress.
#Tamam M. (2015), ''JavaFX Essentials'', APress.
#Vos J. et al. (2014), ''Pro JavaFX 8'', APress.
#Vos J. et al. (2014), ''Pro JavaFX 8'', APress.
 +
#Bauer et. al. (2016), ''Java Persistence with Hibernate'', 2nd Ed., Manning.
 +
#Coehlo H., Kiourtzoglou B., ''Java Persistence API Mini Book'', JavaCodeGeeks.
 +
#Goncalves A. (2013), ''Beginning Java EE 7'', Apress.
 +
#Goncalves A. (2013), "Generating Database Schemas with JPA 2.1", [https://antoniogoncalves.org/2014/12/11/generating-database-schemas-with-jpa-2-1/ Antonio's blog].
 +
#Janssen T. (2016), "Standardized schema generation and data loading with JPA 2.1", [http://www.thoughts-on-java.org/standardized-schema-generation-data-loading-jpa-2-1/ Thoughts-on-java].
 +
#Keith M. & Schincariol M. (2013), ''Pro JPA 2 - Mastering the Java™ Persistence API'', 2nd Ed., APress.

Current revision as of 16:11, 12 April 2017

By John Kostaras


Contents

Introduction

This article is an evolution of the TodoFX application. We shall see how to:

  • add a graph
  • port database access to JPA

Before we continue, please download the TodoFX application from here.

Add a graph

JavaFX has very good support for graphs. We shall extend our TodoFX application by creating a statistics graph to display due date distribution.

We'll use a Bar Chart containing a bar for each month to show how many tasks expire in that particular month.

  1. Right-click on the viewcontroller package and select New --> Other --> JavaFX --> Empty FXML as shown below. Click Next.
  2. Provide TaskStatistics as the FXML Name. Click Next.
  3. Check Use Java Controller and accept the default. Click Next.
  4. Click Finish.

"Figure 12 - Create new FXML file"

  1. Double click on TaskStatistics.fxml to open it in SceneBuilder.
  2. Select the root AnchorPane.
  3. Add a BarChart to the AnchorPane.
  4. Right-click on the BarChart and select Fit to Parent.
  5. Save the .fxml file

Open the TaskStatisticsController and add the following code:

package todofx.viewcontroller;

import java.net.URL;
import java.text.DateFormatSymbols;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.XYChart;
import todofx.model.Task;

/**
 * Task Statistics.
 *
 * @author ikost
 */
public class TaskStatisticsController implements Initializable {

    @FXML
    private BarChart<String, Integer> barChart;

    @FXML
    private CategoryAxis xAxis;

    private final ObservableList<String> monthNames = FXCollections.observableArrayList();

    /**
     * Initializes the controller class.
     */
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // Get an array with the English month names.
        String[] months = DateFormatSymbols.getInstance(Locale.ENGLISH).getMonths();
        // Convert it to a list and add it to our ObservableList of months.
        monthNames.addAll(Arrays.asList(months));

        // Assign the month names as categories for the horizontal axis.
        xAxis.setCategories(monthNames);
    }

    /**
     * Sets the tasks to show the statistics for.
     *
     * @param tasks
     */
    public void setTaskData(List<Task> tasks) {
        // Count the number of tasks expiring in a specific month.
        int[] monthCounter = new int[12];
        tasks.stream()
                .map(t -> t.getDueDate().getMonthValue() - 1)
                .forEach(month -> monthCounter[month]++);

        XYChart.Series<String, Integer> series = new XYChart.Series<>();

        // Create a XYChart.Data object for each month. Add it to the series.
        for (int i = 0; i < monthCounter.length; i++) {
            series.getData().add(new XYChart.Data<>(monthNames.get(i), monthCounter[i]));
        }

        barChart.getData().add(series);
    }

}

The BarChart<String, Integer> uses the String for the months and the Integer for the number of tasks. In the initialize() method, xAxis is initialised with the list of months. setTaskData() will be accessed by Main to set the task data. It loops through all tasks and counts their due dates per month. Then it adds XYChart.Data for every month to the data series. Each XYChart.Data object will represent one bar in the chart.

In SceneBuilder, link the BarChart to the barChart (fx:id) and select the CategoryAxis and connect it to the xAxis property.

"Figure 18 - CategoryAxis"

To connect the TaskStatistics to the rest of the application, add the following method in Main:

    /**
     * Opens a dialog to show task statistics.
     */
    public void showTaskStatistics() {
        try {
            // Load the fxml file and create a new stage for the popup.
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(getClass().getResource("viewcontroller/TaskStatistics.fxml"));
            AnchorPane page = (AnchorPane) loader.load();
            Stage dialogStage = new Stage();
            dialogStage.setTitle("Task Statistics");
            dialogStage.initModality(Modality.WINDOW_MODAL);
            dialogStage.initOwner(primaryStage);
            Scene scene = new Scene(page);
            dialogStage.setScene(scene);

            // Set the tasks into the controller.
            TaskStatisticsController controller = loader.getController();
            try {
                controller.setTaskData(TaskListWrapper.wrap(
                   TaskManagerDB.getInstance().listAllTasks(true)));
            } catch (DatabaseException ex) {
                Logger.getLogger(Main.class.getName())
                      .log(Level.SEVERE, null, ex);
            }

            dialogStage.show();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Finally, open TaskMain in SceneBuilder. Add a new menu Statistics and a new menu item inside it Show Statistics....

Edit TaskMainController by adding the following method:

    /**
     * Opens the task statistics.
     */
    @FXML
    private void handleShowTaskStatistics() {
        mainApp.showTaskStatistics();
    }

Don't forget to link the Show Statistics... menu item to the handleShowTaskStatistics() handler method (Inspector --> Code --> On Action).

If everything went OK, then you should be able to see a dialog box similar to the one below:

"Figure 19 - Task Statistics"

You may try other JavaFX charts:

"Figure 19a - JavaFX charts class hierarchy"

Support for JPA

JDBC is not used anymore in modern applications, when there is a choice to use an Object-Relational Mapping (ORM) library like the standard Java Persistence API (JPA). NetBeans provides very good support for JPA.

1. Right-click on todofx package and select New --> Other --> Persistence --> Entity Classes from Database... and click Next

"Figure 20 - Start the JPA wizard"

2. Select your Database Connection, select the TODO table from the list of Available Tables and click on Add to add it to the list of Selected Tables. Click Next.

"Figure 21 - Select the Database Tables"

3. Edit the class name from Todo to Task. Leave the other settings as in the figure and click on Next.

"Figure 22 - Edit the Entity Classes"

4. Leave the default settings in the Mapping Options page and click Finish.

Your project should be similar to the one shown in the figure:

"Figure 23 - TodoFX project with JPA support"

The wizard has created the persistence.xml file that contains the details about the connection with the database:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
                    http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="TodoFX-JPAPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>todofx.Task</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:file:C:\Users\ikost\db\todo.script"/>
      <property name="javax.persistence.jdbc.user" value="SA"/>
      <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver"/>
      <property name="javax.persistence.jdbc.password" value=""/>
    </properties>
  </persistence-unit>
</persistence>

It has also created todofx.Task class which is our Entity class. If you had chosen todofx.model package instead of todofx, the wizard would have created a Task_1.java file in order not to override Task.java.

Copy the changes from todofx.Task to todofx.model.Task. You may also replace todofx.model.Task with todofx.Task but I 'd personally keep the raw data types (e.g. int, boolean etc.) instead of the generated (Integer, Boolean) ones.

Another issue is with dueDate. JPA 2.1 does not support Java 8, while these lines are being written, so a workaround is needed (which we discuss shortly).

The result should be something like:

package todofx.model;

import java.io.Serializable;
import java.time.LocalDate;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Task
 *
 * @author ikost
 */
@Entity
@Table(name = "TODO")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "Task.findAll", query = "SELECT t FROM Task t"),
    @NamedQuery(name = "Task.findAllOrderByPriority", 
        query = "SELECT t FROM Task t ORDER BY t.priority"),
    @NamedQuery(name = "Task.findAllOrderByDueDate", 
        query = "SELECT t FROM Task t ORDER BY t.dueDate"),    
    @NamedQuery(name = "Task.findById", 
        query = "SELECT t FROM Task t WHERE t.id = :id"),
    @NamedQuery(name = "Task.findByDescription", 
        query = "SELECT t FROM Task t WHERE t.description = :description"),
    @NamedQuery(name = "Task.findByPriority", 
        query = "SELECT t FROM Task t WHERE t.priority = :priority"),
    @NamedQuery(name = "Task.findByCompleted", 
        query = "SELECT t FROM Task t WHERE t.completed = :completed"),
//@NamedQuery(name = "Task.findByDuedate", 
        query = "SELECT t FROM Task t WHERE t.duedate = :duedate"),
    @NamedQuery(name = "Task.findByAlert", 
        query = "SELECT t FROM Task t WHERE t.alert = true"),
//@NamedQuery(name = "Task.findByDaysbefore", 
        query = "SELECT t FROM Task t WHERE t.daysbefore = :daysbefore"),
    @NamedQuery(name = "Task.findByObs", 
        query = "SELECT t FROM Task t WHERE t.obs = :obs")})
public class Task implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "ID")
    private int id;
    @Column(name = "DESCRIPTION")
    private String description;
    @Column(name = "PRIORITY")
    private int priority;
    @Column(name = "DUEDATE")
//    @Convert(converter = LocalDateConverter.class)
    private LocalDate dueDate;
    @Column(name = "ALERT")
    private boolean alert;
    @Column(name = "DAYSBEFORE")
    private int daysBefore;
    @Column(name = "OBS")
    private String obs;
    @Column(name = "COMPLETED")
    private boolean completed;

    public Task() {
        this.id = 0;
    }

    public Task(int id) {
        this.id = id;
    }

    public boolean isLate() {
        LocalDate dateDue = getDueDate();
        return (dateDue == null) ? false : dateDue.compareTo(LocalDate.now()) < 0;
    }

    public boolean hasAlert() {
        LocalDate dateDue = getDueDate();
        if (!getAlert() || dateDue == null) {
            return false;
        } else {
            int dias = dueDate.getDayOfYear() - LocalDate.now().getDayOfYear();
            return dias <= getDaysBefore();
        }
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public LocalDate getDueDate() {
        return dueDate;
    }

    public void setDueDate(LocalDate dueDate) {
        this.dueDate = dueDate;
    }

    public boolean getAlert() {
        return alert;
    }

    public void setAlert(boolean alert) {
        this.alert = alert;
    }

    public int getDaysBefore() {
        return daysBefore;
    }

    public void setDaysBefore(int daysBefore) {
        this.daysBefore = daysBefore;
    }

    public String getObs() {
        return obs;
    }

    public void setObs(String obs) {
        this.obs = obs;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 59 * hash + this.id;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Task other = (Task) obj;
        if (this.id != other.id) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Task{" + "id=" + id + ", description=" + description
                + ", priority=" + priority + ", dueDate=" + dueDate
                + ", alert=" + alert + ", daysBefore=" + daysBefore
                + ", obs=" + obs + ", completed=" + completed + '}';
    }

}

Note! Here you value our design decision not to 'pollute' our Task Value Object with JavaFX properties. Having both JavaFX properties and JPA annotations would make the POJO quite complex and most importantly would mix model with presentation details. If, however, you find a value in doing it, annotate each JavaFX property with @Transient to indicate to JPA not to persist these attributes.

Check a number of important changes in the above code:

  • Two new named queries have been added: "Task.findAllOrderByPriority" and "Task.findAllOrderByDueDate"
  • Named query "Task.findByAlert" has been changed to query = "SELECT t FROM Task t WHERE t.alert = true")
  • It is important that there is a parameterless constructor, and that id is initialised to 0 and not to -1, because the auto-increment will set the first id value to be 0 and EclipseLink has problems with zero keys (see note below).
  • In order to keep dueDate as of type LocalDate, we removed the annotation @Temporal(TemporalType.DATE). But for this to work, we need a converter (in JPA 2.1; JPA 2.2 or 3.0 will provide support for java.time and the following hack will be obsolete):
package todofx.model;

import java.time.LocalDate;
import java.sql.Date;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class LocalDateConverter implements AttributeConverter<LocalDate, Date> {

    @Override
    public Date convertToDatabaseColumn(LocalDate locDate) {
        return locDate == null ? null : Date.valueOf(locDate);
    }

    @Override
    public LocalDate convertToEntityAttribute(Date sqlDate) {
        return sqlDate == null ? null : sqlDate.toLocalDate();
    }
}

to be defined in persistence.xml, too:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="TodoFXPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>todofx.model.Task</class>
    <class>todofx.model.LocalDateConverter</class>
    <properties>
      <property name="javax.persistence.jdbc.url" 
               value="jdbc:hsqldb:file:C:\Users\ikost\db\todo.script"/>
      <property name="javax.persistence.jdbc.user" value="SA"/>
      <property name="javax.persistence.jdbc.driver" 
               value="org.hsqldb.jdbcDriver"/>
      <property name="javax.persistence.jdbc.password" value=""/>
    </properties>
  </persistence-unit>
</persistence>

This converter will apply the conversion java.sql.Date <=> java.time.LocalDate everywhere. If you don't want this, set @Converter(autoApply = false) and in the attributes where you want to apply the conversion, add @Convert(converter = LocalDateConverter.class).

Note! Beware of having a zero id value with EclipseLink (see this discussion). If you or TodoFX accidentally inserted a row in your database with id=0, simply connect to the database and change the 0 to another value or delete the row (see the original article on how to do this). Otherwise you will get weird errors while you are trying to connect. See the above discussion for other solutions.

Now it is time to clean up our project and add our JPA-powered TaskManager. Get rid off DatabaseException.java, ModelException.java, Parameters.java, TaskManager.java, TaskManagerDB.java and todofx.Task.java.

Add the following class:

package todofx.model;

import java.util.List;
import java.util.Optional;
import javax.persistence.EntityManager;
import javax.persistence.Persistence;

/**
 *
 * @author ikost
 */
public class TaskManager {

    private final EntityManager entityManager;

    private TaskManager() {
        entityManager = Persistence.createEntityManagerFactory("TodoFXPU").createEntityManager();
    }

    public String getDatabaseUrl() {
        String dbName = "";
        String url = entityManager.getProperties().get("javax.persistence.jdbc.url").toString();
        if (url != null) {
            dbName = url.substring(url.lastIndexOf("/") + 1);
        }
        return dbName;
    }

    private final static class SingletonHolder {

        private final static TaskManager INSTANCE = new TaskManager();
    }

    public static TaskManager getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public List<Task> listAllTasks(final boolean priorityOrDueDate) {
        String param = priorityOrDueDate ? 
              "Task.findAllOrderByPriority" : "Task.findAllOrderByDueDate";
        return entityManager.createNamedQuery(param).getResultList();
    }

    public List<Task> listTasksWithAlert() {
        return entityManager.createNamedQuery("Task.findByAlert").getResultList();
    }

    public void addTask(final Task task) throws ValidationException {
        validate(task);
        entityManager.getTransaction().begin();
        try {
            entityManager.persist(task);
            entityManager.getTransaction().commit();
        } catch (Exception e) {
            entityManager.getTransaction().rollback();
        } finally {
        }
    }

    public void updateTask(final Task task) throws ValidationException {
        validate(task);
        Task oldTask = findTask(task.getId()).get();
        if (oldTask != null) {
            entityManager.getTransaction().begin();
            try {
                entityManager.persist(task);
                entityManager.getTransaction().commit();
            } catch (Exception e) {
                entityManager.getTransaction().rollback();
            } finally {
            }
        }
    }

    public void markAsCompleted(final int id, final boolean completed) {
        Task task = findTask(id).get();
        if (task != null) {
            task.setCompleted(completed);
            entityManager.getTransaction().begin();
            try {
                entityManager.persist(task);
                entityManager.getTransaction().commit();
            } catch (Exception e) {
                entityManager.getTransaction().rollback();
            } finally {
            }
        }
    }

    public void removeTask(final Task task) {
        entityManager.getTransaction().begin();
        try {
            entityManager.remove(task);
            entityManager.getTransaction().commit();
        } catch (Exception e) {
            entityManager.getTransaction().rollback();
        } finally {
        }
    }

    private boolean isEmpty(final String str) {
        return str == null || str.trim().isEmpty();
    }

    private void validate(final Task task) throws ValidationException {
        if (isEmpty(task.getDescription())) {
            throw new ValidationException("Must provide a task description");
        }
    }

    private Optional<Task> findTask(final int id) {
        return Optional.of((Task) entityManager.find(Task.class, id));
    }
    
}

The only missing functionality is that of the reconnect method which we 'll fix later.

Let's fix our compilation errors. In TaskDetailsDialogController.handleRemove(), change the line with TaskManagerDB to:

TaskManager.getInstance().removeTask(task.getTask());

Similarly TaskMainController has been changed to:

package todofx.viewcontroller;

import java.io.File;
import java.net.URL;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import todofx.Main;
import todofx.model.Task;
import todofx.model.TaskManager;
import todofx.util.DateUtil;

/**
 *
 * @author ikost
 */
public class TaskMainController implements Initializable {

    @FXML
    private TableView<TaskWrapper> taskTable;
    @FXML
    private TableColumn<TaskWrapper, Integer> priorityColumn;
    @FXML
    private TableColumn<TaskWrapper, String> descriptionColumn;
    @FXML
    private TableColumn<TaskWrapper, Boolean> alertColumn;
    @FXML
    private TableColumn<TaskWrapper, LocalDate> dueDateColumn;
    @FXML
    private Label lblMessage;
    @FXML
    private ToggleButton chkShowCompletedTasks;
    @FXML
    private CheckMenuItem chkMnuShowCompletedTasks;
    @FXML
    private ToggleButton chkSortBy;
    @FXML
    private Button btnMarkAsCompleted;
    @FXML
    private ToggleGroup toggleGroup;

    private Main mainApp;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // Add observable list data to the table
        taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
        taskTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        taskTable.setOnMousePressed(event -> {
            if (event.isPrimaryButtonDown() && event.getClickCount() == 2
                    && taskTable.getSelectionModel().getSelectedIndex() > -1) {
                handleEditTask();
            }
        });
        // Initialize the table
        priorityColumn.setCellValueFactory(cellData -> 
            cellData.getValue().getPriorityProperty().asObject());
        descriptionColumn.setCellValueFactory(cellData -> 
            cellData.getValue().getDescriptionProperty());
        alertColumn.setCellValueFactory(cellData -> 
            cellData.getValue().getAlertProperty());
        dueDateColumn.setCellValueFactory(cellData -> 
            cellData.getValue().getDueDateProperty());
        dueDateColumn.setCellFactory(column -> {
            return new TableCell<TaskWrapper, LocalDate>() {
                @Override
                protected void updateItem(LocalDate item, boolean empty) {
                    super.updateItem(item, empty);
                    if (item == null || empty) {
                        setText(null);
                        setStyle("");
                    } else {
                        setText(DateUtil.format(item));
                    }
                }
            };
        });
        final String defaultStyle = taskTable.getStyle();
        taskTable.setRowFactory(tableView -> {
            return new TableRow<TaskWrapper>() {
                @Override
                protected void updateItem(TaskWrapper item, boolean empty) {
                    super.updateItem(item, empty);
                    if (!empty) {
                        if (item.isCompleted()) {
                            setStyle("-fx-background-color: #95caff;");
                        } else if (item.isLate()) {
                            setStyle("-fx-background-color: #f06f06;");
                        } else if (item.hasAlert()) {
                            setStyle("-fx-background-color: #ffff00;");
                        } else {
                            setStyle(defaultStyle);
                        }
                    } else {
                        setStyle(defaultStyle);
                    }
                }
            };
        });
        handleShowAlerts();
    }

    /**
     * Called when the user clicks on New Task List.
     */
    @FXML
    private void handleNewTaskList() {
        FileChooser dlg = new FileChooser();
        dlg.setTitle("New Task List");
        dlg.getExtensionFilters().addAll(
                new ExtensionFilter("Task Lists - HSQLDB Databases (*.script)", "*.script"),
                new ExtensionFilter("All Files", "*.*")
        );
        File dir = new File(TaskManager.getInstance().getDatabaseUrl()).getParentFile();
        dlg.setInitialDirectory(dir);
        File selectedFile = dlg.showSaveDialog(mainApp.getPrimaryStage());
        if (selectedFile != null) {
            try {
                String arq = createOpenDatabase(selectedFile);
                setStatus("Task list created: " + arq, false);
            } catch (Exception e) {
                setStatus("Cannot create task list", true);
            }
        }
    }

    /**
     * Called when the user clicks on Open Task List.
     */
    @FXML
    private void handleOpenTaskList() {
        FileChooser dlg = new FileChooser();
        dlg.setTitle("Open Task List");
        dlg.getExtensionFilters().addAll(
                new ExtensionFilter("Task Lists - HSQLDB Databases (*.script)", "*.script"),
                new ExtensionFilter("All Files", "*.*")
        );
        File dir = new File(TaskManager.getInstance().getDatabaseUrl()).getParentFile();
        dlg.setInitialDirectory(dir);
        File selectedFile = dlg.showOpenDialog(mainApp.getPrimaryStage());
        if (selectedFile != null) {
            try {
                String arq = createOpenDatabase(selectedFile);
                setStatus("Task List opened: " + arq, false);
            } catch (Exception e) {
                setStatus("Cannot open task list", true);
            }
        }
    }

    private String createOpenDatabase(File databaseFile) {
        String fileName = databaseFile.getAbsolutePath();
        if (fileName.startsWith("file:")) {
            fileName = fileName.substring(5);
        }
        if (fileName.endsWith(".script")) {
            fileName = fileName.substring(0, fileName.length() - 7);
        }
//        TaskManager.getInstance().reconnect(fileName);
        taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
        return fileName;
    }

    /**
     * Called when the user clicks Show Alerts.
     */
    @FXML
    private void handleShowAlerts() {
        List<Task> tasksWithAlert = new ArrayList<>();
        tasksWithAlert = TaskManager.getInstance().listTasksWithAlert();
        tasksWithAlert.stream().forEach(t -> {
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setTitle("Alert");
            alert.setHeaderText("Task: " + t.getDescription());
            alert.setContentText("expired on " + DateUtil.format(t.getDueDate()));
            alert.showAndWait();
        });
        if (tasksWithAlert.isEmpty()) {
            setStatus("There are no tasks with alerts for today.", false);
        } else {
            setStatus("There are " + tasksWithAlert.size() + " task(s) with alerts for today.", false);
        }
    }

    @FXML
    private void handleAbout() {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("About TodoFX");
        alert.setHeaderText("TodoFX - Task List\nRelease 1.0\n\n"
                + "                http://wiki.netbeans.org/TodoFX");
        alert.setContentText("Author: Ioannis Kostaras");
        alert.showAndWait();
    }

    @FXML
    private void handleNewTask() {
        Task tempTask = new Task();
        boolean saveClicked = mainApp.showTaskDetailsDialog(new TaskWrapper(tempTask));
        if (saveClicked) {
            try {
                TaskManager.getInstance().addTask(tempTask);
            } catch (ValidationException ex) {
                Logger.getLogger(TaskMainController.class.getName()).log(Level.SEVERE, null, ex);
            }
            // Add observable list data to the table
            taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
        }
    }

    @FXML
    private void handleEditTask() {
        TaskWrapper selectedTask = taskTable.getSelectionModel().getSelectedItem();
        if (selectedTask != null) {
            boolean saveClicked = mainApp.showTaskDetailsDialog(selectedTask);
            if (saveClicked) {
                try {
                    TaskManager.getInstance().updateTask(selectedTask.getTask());
                } catch (ValidationException ex) {
                    Logger.getLogger(TaskMainController.class.getName()).log(Level.SEVERE, null, ex);
                }
                taskTable.refresh();
            }
        } else {
            // Nothing selected.
            Alert alert = new Alert(Alert.AlertType.WARNING);
            alert.setTitle("No Selection");
            alert.setHeaderText("No Task Selected");
            alert.setContentText("Please select a task in the table.");

            alert.showAndWait();
        }
    }

    @FXML
    private void handleRemoveTask() {
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
        selectedTasks.stream().forEach(task -> {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Remove Task");
            alert.setHeaderText("Are you sure you want to remove task? ");
            alert.setContentText(task.getDescription());
            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == ButtonType.OK) {
                taskTable.getItems().remove(task);
                TaskManager.getInstance().removeTask(task.getTask());
                taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
            }
        });
    }

    /**
     * Called when the user clicks Show Completed Tasks.
     */
    private void showCompletedTasks(boolean show) {
        if (show) {
            taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true))
                    .filtered(t -> t.isCompleted()));
        } else {
            taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(true)));
        }
    }

    /**
     * Called when the user clicks on Show Completed Tasks check button.
     */
    @FXML
    private void handleButtonShowCompletedTasks() {
        showCompletedTasks(chkShowCompletedTasks.isSelected());
        chkMnuShowCompletedTasks.setSelected(chkShowCompletedTasks.isSelected());
    }

    /**
     * Called when the user clicks Show Completed Tasks check menu item.
     */
    @FXML
    private void handleMenuItemShowCompletedTasks() {
        showCompletedTasks(chkMnuShowCompletedTasks.isSelected());
        chkShowCompletedTasks.setSelected(chkMnuShowCompletedTasks.isSelected());
    }

    /**
     * Called when the user clicks Sort By Priority/Due Date check button.
     */
    @FXML
    private void handleSortBy() {
        sortBy(chkSortBy.isSelected());
    }

    /**
     * @param byPriority if {@code true} sort tasks by priority, otherwise by
     * due date
     */
    private void sortBy(boolean byPriority) {
        taskTable.setItems(TaskListWrapper.wrap(TaskManager.getInstance().listAllTasks(byPriority)));
        chkSortBy.setSelected(byPriority);
    }

    /**
     * Called when the user clicks Sort By Priority menu item.
     */
    @FXML
    private void handleSortByPriority() {
        sortBy(true);
    }

    /**
     * Called when the user clicks Sort By Due Date menu item.
     */
    @FXML
    private void handleSortByDueDate() {
        sortBy(false);
    }

    /**
     * Called when the user clicks Mark As Completed.
     */
    @FXML
    private void handleMarkAsCompleted() {
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
        selectedTasks.forEach(selectedTask -> {
            if (selectedTask != null) {
                selectedTask.setCompleted(true);
                TaskManager.getInstance().markAsCompleted(selectedTask.getId(), true);
            } else {
                Alert alert = new Alert(Alert.AlertType.WARNING);
                alert.setTitle("Warning");
                alert.setHeaderText("No Task Selected");
                alert.setContentText("Select a task first");
                alert.showAndWait();
            }
        });
        taskTable.refresh();
    }

    /**
     * Opens the task statistics.
     */
    @FXML
    private void handleShowTaskStatistics() {
        mainApp.showTaskStatistics();
    }

    /**
     * Closes the application.
     */
    @FXML
    private void handleExit() {
        System.exit(0);
    }

    public void setMainApp(Main mainApp) {
        this.mainApp = mainApp;
    }

    public void setStatus(String msg, boolean isError) {
        lblMessage.setText(msg);
        if (isError) {
            lblMessage.setTextFill(Color.RED);
        } else {
            lblMessage.setTextFill(Color.BLACK);
        }
    }
}

Clean and build and run your application.

Note! When you clean and build the project, you might encounter an exception class file for sun.util.logging.PlatformLogger not found. Fortunately, there is an easy solution:

  1. Right Click on the project and select "Properties".
  2. Select "Libraries" from opened "Project Properties window".
  3. Select "Processor" tab.
  4. Remove EclipseLink from there.
  5. Click on OK and do a Clean Build.

The application should behave as before, apart from the two menu items File --> New task list and File --> Open task list.

The program works because you have an already created db/todo database. But if you don't then it will fail. We need to create an empty database schema, if there isn't one.

JPA 2.1 added, finally, support for creating the database schema. According to references [11-13] and this tutorial, you need to add these properties to your persistence.xml

      <property name="javax.persistence.schema-generation.database.action" value="create"/>
      <property name="javax.persistence.schema-generation.scripts.action" value="create"/>
      <property name="javax.persistence.schema-generation.scripts.create-target" value="resources/sql/createDB.ddl"/>
      <property name="javax.persistence.schema-generation.scripts.drop-target" value="resources/sql/dropDB.ddl"/> 
      <!--<property name="javax.persistence.sql-load-script-source" value="resources/sql/insert.sql"/>-->  

and create the following .sql scripts inside resources/sql folder:

//createDB.sql
CREATE TABLE PUBLIC.todo (
    id IDENTITY, 
    description VARCHAR(100), 
    priority INTEGER, 
    completed BOOLEAN, 
    dueDate DATE, 
    alert BOOLEAN, 
    daysBefore INTEGER, 
    obs VARCHAR(250)
)

//dropDB.sql
DROP TABLE PUBLIC.todo;

and an optional insert.sql with INSERT SQL statements to insert sample data on table creation (here we have commented it out). To create an empty SQL file with NetBeans, click on File --> New --> Other --> SQL file.

The final step is to add the following line to the beginning of the constructor of TaskManager:

Persistence.generateSchema("TodoFXPU", null);

Now, if the db/todo database does not exist, it will be created the first time the program is executed. Don't forget to clean and build for the changes in persistence.xml to take effect.

But we still haven't created a reconnect() method. Add the following to TaskManager:

public class TaskManager {

    private EntityManager entityManager;
    private final Map<String, Object> properties;
    private static final String DEFAULT_URL = "jdbc:hsqldb:file:";

    private TaskManager() {
        Persistence.generateSchema("TodoFXPU", null);
        entityManager = Persistence.createEntityManagerFactory("TodoFXPU").createEntityManager();
        properties = new HashMap<>(entityManager.getProperties());
    }

    public void reconnect(String database) {
        entityManager.close();
        properties.put("javax.persistence.jdbc.url", DEFAULT_URL + database);
        Persistence.generateSchema("TodoFXPU", properties);
        entityManager = Persistence.createEntityManagerFactory("TodoFXPU", properties).createEntityManager();
    }    

    ...

The values of the properties hash map override any properties read by persistence.xml.

Finally, in TaskMainController, method createOpenDatabase() uncomment the reconnect() method:

   private String createOpenDatabase(File databaseFile) {
       String fileName = databaseFile.getAbsolutePath();
       if (fileName.startsWith("file:")) {
           fileName = fileName.substring(5);
       }
       if (fileName.endsWith(".script")) {
           fileName = fileName.substring(0, fileName.length() - 7);
       }
       TaskManager.getInstance().reconnect(fileName);
       taskTable.setItems(TaskListWrapper.wrap(
                 TaskManager.getInstance().listAllTasks(true)));
       return fileName;
   }

Rebuild and test the application to see that it behaves like the initial Swing Todo one.

Recap

In this tutorial we show how we can port a Java Swing standalone application to JavaFX using the NetBeans IDE. We also show how to add graphs and how to port JDBC to JPA. We tried to keep a good design following the Model-View-Controller (MVC) design pattern.

You may compare it to the original article as well as to the TodoRCP version.

References

  1. Lozano F. (2006), "A complete App using NetBeans 5", NetBeans Magazine, Issue 1, May, http://netbeans.org/download/magazine/01/nb01_completeapp.pdf
  2. Anderson G., Anderson P. (2014), JavaFX Rich Client Programming on the Netbeans Platform, Addison-Wesley.
  3. Dea C. et al. (2014), JavaFX 8 Introduction by Example, 2nd Ed., APress.
  4. Duodu E. (2015), "How to Create a JavaFX GUI using Scene Builder in NetBeans", IDR Solutions.
  5. Jacob M. (2014), "JavaFX 8 Tutorial", code.makery.
  6. Sharan K. (2014), Learn JavaFX 8, APress.
  7. Tamam M. (2015), JavaFX Essentials, APress.
  8. Vos J. et al. (2014), Pro JavaFX 8, APress.
  9. Bauer et. al. (2016), Java Persistence with Hibernate, 2nd Ed., Manning.
  10. Coehlo H., Kiourtzoglou B., Java Persistence API Mini Book, JavaCodeGeeks.
  11. Goncalves A. (2013), Beginning Java EE 7, Apress.
  12. Goncalves A. (2013), "Generating Database Schemas with JPA 2.1", Antonio's blog.
  13. Janssen T. (2016), "Standardized schema generation and data loading with JPA 2.1", Thoughts-on-java.
  14. Keith M. & Schincariol M. (2013), Pro JPA 2 - Mastering the Java™ Persistence API, 2nd Ed., APress.
Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo