Simple JavaFx Application Base
Documentation
Version 2.0.0

Requirements

This Java framework was built by using JDK 17. Please make sure that your project is using at least JDK 17 too.

Dependencies

The whole framework can be plugged in into your project by just adding a dependency. All needed dependencies are located in the central maven repository. There is no need to add third party repositories.

Gradle

Groovy
repositories {
    mavenCentral()
}
dependencies {
    implementation("com.wedasoft:simplejavafxapplicationbase:2.0.0")
    implementation("org.hibernate:hibernate-core:6.2.6.Final")

    // optional: h2 database
    implementation("com.h2database:h2:2.2.220")

    // optional: hibernate metamodel generator
    annotationProcessor("org.hibernate:hibernate-jpamodelgen:6.2.6.Final")
}

Maven

XML
<!-- Maven looks in the central repository by default. -->
<dependency>
    <groupId>com.wedasoft</groupId>
    <artifactId>simplejavafxapplicationbase</artifactId>
    <version>2.0.0</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.2.6.Final</version>
    <type>pom</type>
</dependency>

<!-- optional: h2 database -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.220</version>
    <scope>test</scope>
</dependency>

<!-- optional: hibernate metamodel generator -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <version>6.2.6.Final</version>
    <type>pom</type>
    <scope>provided</scope>
</dependency>

Plain JAR

Please search in the internet for a manual about the topic “how to include a jar dependency in my java project with IDE “.

Compile dependency manually

Fully automated on Windows

#1: Do NOT clone the repository, etc.

#2: Execute the batch script “download_and_compile_script.bat” from the project root from github.

#3: The script will do everything for you. Downloading the git repository, executing the gradle task and publishing to your local maven repository.

Fully automated on Linux

Linux is cool though, but the shell script is not written yet. Please compile the JAR manually.

Manually on Windows

#1 Download the git repository via:

Markdown
git clone https://github.com/davidweber411/SimpleJavaFxApplicationBase

#2 Navigate to the project root via:

Markdown
cd SimpleJavaFxApplicationBase

#3 Run the gradle task publishToMavenLocal via:

Markdown
.gradlew publishToMavenLocal

The JAR should be located in your local maven repo – regularly here:

Markdown
"C:Users%username%.m2repositorycomwedasoftSimpleJavaFxApplicationBase..."
Manually on Linux

#1 Download the git repository via:

Markdown
git clone https://github.com/davidweber411/SimpleJavaFxApplicationBase

#2 Navigate to the project root and run the gradle task publishToMavenLocal via:

Markdown
cd SimpleJavaFxApplicationBase

#3 Run the gradle task publishToMavenLocal via:

Markdown
./gradlew publishToMavenLocal

The JAR should be located in your local maven repo – regularly here:

Markdown
"~/.m2/repository/com/wedasoft/SimpleJavaFxApplicationBase/..."

Critical version changes

  • The SceneSwitcher is not available anymore. You must refactor your code to use “SceneUtil.switchSceneRoot()”.
  • The fxml dialog builder is not available anymore. You must refactor your code to use “JfxDialogUtil.createAndShowFxmlDialog()”.

Dialog API

This API is used to create highly customizable fxml based dialogs and to make use of predefined common dialogs. Benefit from predefined information-, warning-, error-, confirmation- and input dialogs.

Common dialogs

Create common dialogs never by yourself again. Benefit from predefined information-, warning-, error-, confirmation- and input dialogs.

Information dialogs
Java
JfxDialogUtil.createInformationDialog(String message)

JfxDialogUtil.createInformationDialog(String message, String messageHeader)
Warning dialogs
Java
JfxDialogUtil.createWarningDialog(String message)

JfxDialogUtil.createWarningDialog(String message, String messageHeader)
Error dialogs
Java
JfxDialogUtil.createErrorDialog(String message)
 
JfxDialogUtil.createErrorDialog(String message, Exception exceptionForStacktrace)
Input dialogs
Java
JfxDialogUtil.displayInputDialogAndGetResult(String dialogText)
Confirm dialogs
Java
JfxDialogUtil.displayConfirmDialogAndGetResult(String headerText, String contentText)
Other dialogs
Java
JfxDialogUtil.displayCloseStageDialog(Stage stageToClose)

JfxDialogUtil.displayCloseStageDialog(Stage stageToClose, String dialogTitle, String dialogText)

JfxDialogUtil.displayExitProgramDialog()

JfxDialogUtil.displayExitProgramDialog(String dialogTitle, String dialogText)

Individual FXML dialogs

This API is used to create and configure individual FXML based dialogs. You can pass and retrieve arguments without any effort. Trust me: It won’t get more simple than this.

Step 1: Prepare the dialog’s fxml file

Create a standard fxml file and link your controller.

XML
<?xml version="1.0" encoding="UTF-8"?>

<VBox xmlns:fx="http://javafx.com/fxml"
   fx:controller="com.example.abc.YourController">
   ...
</VBox>
Step 2: Prepare the dialog’s controller
Java
public class YourController {
     
       public void init(String parameter1) {
            // Use this method as init. It can have any name.
       }

}
Step 3: Create and open the dialog
Java
JfxDialogUtil.createAndShowFxmlDialog(
    /* title */                  "My Dialog title",
    /* dialogIsModal */          true,
    /* dialogIsResizeable */     false,
    /* absoluteFxmlFileUrl */    getClass().getResource("/com/example/abc/yourScene.fxml"),
    /* sceneSize or null */      new Dimension2D(600, 500),
    /* initMethodOfController */ (Consumer<YourController>) consumer -> consumer.init("myparamter1"),
    /* callbackOnDialogClose */  () -> integerValueChangedByCallback = 52)

Scene API

This API is used to switch the content (root) of a scene determined by its stage. You can pass arguments and compute them with ease.

This API is meant to be used with FXML scenes.

Step 1: Prepare the scene’s fxml file

XML
<?xml version="1.0" encoding="UTF-8"?>

<VBox xmlns:fx="http://javafx.com/fxml"
   fx:controller="com.example.abc.YourController">
   ...
</VBox>

Step 2: Prepare the scene’s controller

Java
public class YourController {
     
       public void init(String parameter1) {
            // Use this method as init. It can have any name.
       }

}

Step 3: Switch the Scene

Java
SceneUtil.switchSceneRoot(
       SceneUtil.getStageByActionEvent(event),
       /* SceneUtil.getStageByChildNode(node), */
       /* SceneUtil.getStageByScene(scene), */
       getClass().getResource("/com/example/abc/yourScene.fxml"),
       (Consumer<YourController >) controller -> controller.init("myparamter1"));

Database API

This API is used to create, read, update and delete your entity/entities. No need for writing boilerplate code anymore. The complete API is built on top of the Hibernate API – feel free to use it with every database, that is supported by Hibernate. To that, you can benefit from a simple but powerful finding API using the builder pattern.

The “entry class” of this API is always HibernateQueryUtil – all interactions are starting at this class. This class contains some subclasses for grouping functionalities. The subclasses are named example given Inserter, Updater, Deleter and Finder.

All standard CRUD operations are implemented generically, so you do not need to write boilerplate code for this actions. A session with an associated transaction is created for every action.

Setup

Step 1: Create your entities

You need create your entities (database tables) as classes. In your entity classes you can use the standard hibernate annotations. It is recommended to define column names with @Column. For any further information please take a look at the official hibernate documentation at https://hibernate.org.

Hint: Don’t forget the empty constructor, it is required by hibernate to work.

Please take a look at the example entity Student below:

Java
import jakarta.persistence.*;

@Entity
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

    @Column(name = "age")
    private int age;

    // Needed by Hibernate.
    public Student() {
    }

    // The parameter id (Long) must be null for creation.
    public Student(String firstName, String lastName, String email, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.age = age;
    }

    public Student(String firstName, String lastName, String email) {
        this(firstName, lastName, email, -1);
    }

    Getters / Setters ...

}
Step 2: Configure your database

You need create the file hibernate.cfg.xml in the resources directory of your project. This file is used by hibernate to configure your database. The shown example file below is using the H2 database and registers/maps only a single entity called Student. If you need more information about how to configure hibernate then please take a look at the official documentation at https://hibernate.org.

XML
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <!-- JDBC Database connection settings (creates the db with this values) -->
        <property name="connection.driver_class">org.h2.Driver</property>
        <property name="connection.username">sa</property>
        <property name="connection.password">sa</property>

        <!-- Use an in memory database named "test.db" (no file on your computer) -->
        <property name="connection.url">jdbc:h2:mem:test</property>

        <!-- Use a database named "database.db" located at "user-home/yourCompany/yourAppName/db/database.db" -->
        <!-- <property name="connection.url">jdbc:h2:file:~/yourCompany/yourAppName/db/database;AUTO_SERVER=TRUE</property> -->

        <!-- JDBC connection pool settings ... using built-in test pool -->
        <property name="connection.pool_size">1</property>

        <!-- Select our SQL dialect -->
        <property name="dialect">org.hibernate.dialect.H2Dialect</property>

        <!-- Echo the SQL to stdout -->
        <property name="show_sql">true</property>

        <!-- Set the current session context -->
        <property name="current_session_context_class">thread</property>

        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">create-drop</property>
        <!-- Other possible values: -->
        <!-- none:         does nothing with the schema/database. -->
        <!-- validate:     database schema will be validated using the entity mappings. -->
        <!-- create-only:  database schema creation will be generated. -->
        <!-- drop:         database schema will be dropped. -->
        <!-- create:       database schema will first be dropped and then created afterward. -->
        <!-- create-drop:  database schema will be created and will be dropped when the SessionFactory is closed explicitly (application stops). -->
        <!-- update:       database schema will be updated by comparing the existing database schema with the entity mappings. -->

        <!-- dbcp connection pool configuration -->
        <property name="hibernate.dbcp.initialSize">5</property>
        <property name="hibernate.dbcp.maxTotal">20</property>
        <property name="hibernate.dbcp.maxIdle">10</property>
        <property name="hibernate.dbcp.minIdle">5</property>
        <property name="hibernate.dbcp.maxWaitMillis">-1</property>

        <!-- mappings from your classes to hibernate/database -->
        <mapping class="com.company.app.entities.Student"/>
    </session-factory>
</hibernate-configuration>
Step 3: Start your application

Hibernate will create the complete database and all configured tables (configured with your EntityClasses and mapped in your hibernate.cfg.xml file) in your database automatically when your application starts. For any further information please take a look at the official hibernate documentation at https://hibernate.org.

It is recommended that you install a database explorer for your database. Feel free to use any database explorer you like. Below is an example of the configurations if you are using a h2 database.

If you use IntelliJ IDEA, you can use an embedded database browser with the plugin database navigator. Create a custom connection type.
Enter the following values in the custom connection:
URL: The value of connection.url of the hibernate.cfg.xml
User: The value of connection.username of the hibernate.cfg.xml
Password: The value of connection.password of the hibernate.cfg.xml
Driver library: Path to your database driver stored in your local maven repository, e.g. :
C:\<userhome>\m2\repository\com\h2database\h2\2.1.214\h2-2.1.214.jar

Alternatively you can download a database explorer directly from H2 at http://www.h2database.com/html/quickstart.html#h2_console. If you use another database, then you need to search for a database explorer, which is compatible with your database.

Insert data

This will simply do the trick:

Java
HibernateQueryUtil.Inserter.insertOne(T entity)
HibernateQueryUtil.Inserter.insertMany(List<T> entities)

Update data

This will simply do the trick:

Java
HibernateQueryUtil.Updater.updateOne(T entity)
HibernateQueryUtil.Updater.updateMany(List<T> entities)

Delete data

This will simply do the trick:

Java
HibernateQueryUtil.Deleter.deleteOne(T entity) 
HibernateQueryUtil.Deleter.deleteMany(List<T> entities) 
HibernateQueryUtil.Deleter.deleteAll(Class<T> entityClass, boolean areYouSure)

Find data

Usage

The following code, invoked with the builder pattern, will simply do the trick:

Java
HibernateQueryUtil.Finder.findWithBuilder(Student.class)
    .addCondition(Student_.FIRST_NAME, isEqualTo("David")) // 1. where ...
    .addCondition(Student_.ID, isEqualTo(27)) // 2. where ...
    .offset(10) // skip the first 10 entries
    .limit(10) // get only 10 entries
    .orderByInOrderOfList(List.of(
        new Order(Student_.ID, true), // 1. order by id ASC
        new Order(Student_.LAST_NAME, true), // 2. order by lastName ASC 
        new Order(Student_.FIRST_NAME, false))) // 3. order by firstName DESC
    .findAll();
Explanation for the database API:

The constants for the class field names like Student_.FIRST_NAME are generated automatically by hibernate for every class, that is annotated with @Entity. For Gradle, the used hibernate dependency for this feature is:

Groovy
// https://mvnrepository.com/artifact/org.hibernate/hibernate-jpamodelgen
annotationProcessor("org.hibernate:hibernate-jpamodelgen:6.2.4.Final")

The generated constants represent the column names in your database tables. You don’t need to worry about how the fields are named in your database. The database API will compute the correct column name. In case, that you use the annotation @Column and pass a column name as a parameter, the database API will use the passed column name. If you don’t define a column name, the database API will use the class field name.

Java
// Person_.firstName will return "first_name" in this case.
@Column(name = "first_name")
private String firstName;

// Person_.firstName will return "firstName" in this case.
@Column
private String firstName;
Explanation for the hibernate dependency:

If your class is named YourEntity, the corresponding generated metadata class by hibernate is called YourEntity_. All metadata classes are generated automatically when your project compiles. If there are no metadata classes in your project, you need to compile it the first time manually.

Other condition types and examples
Java
// Matches everything, that is exactly equal to "David":
    .addCondition(Student_.<FIELD_NAME>, isEqualTo("David"))

// Matches everything, that is lower than 4:
    .addCondition(Student_.<FIELD_NAME>, isLowerThan(4))

// Matches everything, that is lower than or equal to 4:
    .addCondition(Student_.<FIELD_NAME>, isLowerThanOrEqualTo(4))

// Matches everything, that is greater than 4:
    .addCondition(Student_.<FIELD_NAME>, isGreaterThan(4))

// Matches everything, that is greater than or equal to 4:
    .addCondition(Student_.<FIELD_NAME>, isGreaterThanOrEqualTo(4))

// Matches everything, that is not equal to 4:
    .addCondition(Student_.<FIELD_NAME>, isNotEqualTo(4))

// Matches everything, that starts exactly with "Dav":
    .addCondition(Student_.<FIELD_NAME>, isLikeCaseSensitive("Dav%"))

// Matches everything, that ends exactly with "vid":
    .addCondition(Student_.<FIELD_NAME>, isLikeCaseSensitive("%vid"))

// Matches everything, that contains exactly "avi":
    .addCondition(Student_.<FIELD_NAME>, isLikeCaseSensitive("%avi%"))

// Matches everything, that does not contain exactly "avi":
    .addCondition(Student_.<FIELD_NAME>, isNotLikeCaseSensitive("%avi%"))

// Matches everything, that does not start with exactly "Dav":
    .addCondition(Student_.<FIELD_NAME>, isNotLikeCaseSensitive("Dav%"))

// Matches everything, that contains "avi" in every lower- and uppercase combination ("avi", "Avi", "AVi", "aVI", ...):
    .addCondition(Student_.<FIELD_NAME>, isLikeInCaseSensitive("%avi%"))

// Matches everything, that does not start with "dav" in every lower- and uppercase combination ("dav", "Dav", "DAV", "dAv", ...):
    .addCondition(Student_.<FIELD_NAME>, isNotLikeInCaseSensitive("dav%"))

// Matches everything, that does not contain "avi" in every lower- and uppercase combination ("avi", "Avi", "AVi", "aVI", ...):
    .addCondition(Student_.<FIELD_NAME>, isNotLikeInCaseSensitive("%avi%"))

Other things with data

Counting datasets
Java
HibernateQueryUtil.Finder.countAll(Class<T> entityClass)

Testing suite

This API allows you to easily create and run tests that require JavaFX components with ease, just by extending your test class with one single class. No more annoying errors like “Toolkit not found”, “Toolkit already initialized”, “Location is not set” or “Not on FX application thread” during testing.

Step 1: Prepare the test class(es)

Extend your wished test class(es) with the class SimpleJavaFxTestBase. That’s it.

Java
class UnitTests extends SimpleJavaFxTestBase { 
}

Step 2: Write a unit test

Simply write a standard unit test method in your test class.

To run code on the JavaFX thread, just invoke runOnJavaFxThreadAndJoin() and pass the code.
The main thread will wait until the passed code is executed.
You can invoke runOnJavaFxThreadAndJoin() as often as you like.

IMPORTANT:
Do not use assertions in runOnJavaFxThreadAndJoin().
JUnit will not recognize failed assertions in the JavaFX thread.

Java
@Test
void myTest1() throws Exception {
    runOnJavaFxThreadAndJoin(() -> {
        // your main thread will wait for this code to complete
        // this code runs on the JavaFX thread
        button = new Button("buttonlabel");
        // after executing this code, the main thread will go ahead
    });
    assertEquals("buttonlabel", button.getText());
}

Example test class

Java
import javafx.geometry.Dimension2D;
import javafx.scene.control.Button;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class SimpleJavaFxTestBaseTestJfxInitFunction extends SimpleJavaFxTestBase {

    FxmlDialog.Builder builder;
    Button button;

    @Test
    void testButton() throws Exception {
        runOnJavaFxThreadAndJoin(() -> {
            button = new Button("buttonLabel");
        });
        assertEquals("buttonLabel", button.getText());
        assertNotEquals("wrongLabel", button.getText());
    }

    @Test
    void testDialogBuilding() throws Exception {
        runOnJavaFxThreadAndJoin(() -> {
            builder = new FxmlDialog.Builder(getClass().getResource("/com/wedasoft/simplejavafxtestbase/test_woc.fxml"), new Dimension2D(600, 500));
            builder.setStageTitle("Old StageTitle");
        });
        assertEquals("Old StageTitle", builder.get().getStage().getTitle());
        assertNotNull(builder.get());
        builder.setStageTitle("New StageTitle");
        assertEquals(600, builder.get().getStage().getScene().getWidth());
        assertEquals(500, builder.get().getStage().getScene().getHeight());
        assertEquals("New StageTitle", builder.get().getStage().getTitle());
    }

    @Test
    void multiRunsOnJavaFxThread() throws Exception {
        runOnJavaFxThreadAndJoin(() -> {
            builder = new FxmlDialog.Builder(getClass().getResource("/com/wedasoft/simplejavafxtestbase/test_woc.fxml"), new Dimension2D(600, 500));
            builder.setStageTitle("Old StageTitle");
        });
        assertNotNull(builder.get());
        assertEquals("Old StageTitle", builder.get().getStage().getTitle());
        assertEquals(600, builder.get().getStage().getScene().getWidth());
        assertEquals(500, builder.get().getStage().getScene().getHeight());

        builder.setStageTitle("New StageTitle");
        assertEquals("New StageTitle", builder.get().getStage().getTitle());

        assertThrows(Exception.class, () -> runOnJavaFxThreadAndJoin(() -> builder = new FxmlDialog.Builder(getClass().getResource("/path/does/not/exist/test_woc.fxml"), new Dimension2D(600, 500))));

        runOnJavaFxThreadAndJoin(() -> {
            builder = new FxmlDialog.Builder(getClass().getResource("/com/wedasoft/simplejavafxtestbase/test_woc.fxml"), new Dimension2D(1000, 1000));
            builder.setStageTitle("Another StageTitle");
            button2 = new Button("second init button");
        });
        assertEquals(1000, builder.get().getStage().getScene().getWidth());
        assertEquals(1000, builder.get().getStage().getScene().getHeight());
        assertEquals("Another StageTitle", builder.get().getStage().getTitle());
        
        assertNotNull(button2);
        assertEquals("second init button", button2.getText());
    }

}