1. Rich client

JMinor is primarily a Swing based framework, the Swing client is mature and stable while the JavaFX client is quite rudimentary and still in the 'proof-of-concept' stage. The below artifacts pull in all required framework dependencies.

Client Artifact

Swing

org.jminor.jdk11:jminor-swing-framework.ui:0.14.1

JavaFX

org.jminor.jdk11:jminor-javafx-framework:0.14.1

2. Database connectivity

A JMinor client has three ways of connecting to a database, directly via a local JDBC connection or remotely via RMI or HTTP using the JMinor remote server.

DB Connection Artifact

Local

org.jminor.jdk11:jminor-framework-db-local:0.14.1

RMI

org.jminor.jdk11:jminor-framework-db-remote:0.14.1

HTTP

org.jminor.jdk11:jminor-framework-db-http:0.14.1

3. DBMS

When connecting to the database with a local JDBC connection the DBMS module for the underlying database must be on the classpath. Note that these artifacts do not depend on the JDBC drivers, so those must be added separately.

The most used and tested DBMS modules are:

  1. Oracle

  2. H2 Database

  3. Postgresql

DBMS Artifact

Derby

org.jminor.jdk11:jminor-dbms-derby:0.14.1

H2 Database

org.jminor.jdk11:jminor-dbms-h2database:0.14.1

HSQL

org.jminor.jdk11:jminor-dbms-hsql:0.14.1

MariaDB

org.jminor.jdk11:jminor-dbms-mariadb:0.14.1

MySQL

org.jminor.jdk11:jminor-dbms-mysql:0.14.1

Oracle

org.jminor.jdk11:jminor-dbms-oracle:0.14.1

Postgresql

org.jminor.jdk11:jminor-dbms-postgresql:0.14.1

SQLite

org.jminor.jdk11:jminor-dbms-sqlite:0.14.1

SQL Server

org.jminor.jdk11:jminor-dbms-sqlserver:0.14.1

4. Logging

JMinor uses SLF4J throughout so all you need to do is add a SLF4J bridge for your logging framework of choice to the classpath. If you use Logback or Log4J you can use one of the logging-proxy plugins below which will pull in the required dependencies and provide a main-menu action in the client for setting the logging level.

Logging Artifact

Logback

org.jminor.jdk11:jminor-plugin-logback-proxy:0.14.1

Log4j

org.jminor.jdk11:jminor-plugin-log4j-proxy:0.14.1

Java Util Logging

org.jminor.jdk11:jminor-plugin-jul-proxy:0.14.1

5. Gradle

dependencies {
    //Swing client UI module
    implementation 'org.jminor.jdk11:jminor-swing-framework-ui:0.14.1'

    //JSON for persisting client preferences
    runtimeOnly 'org.jminor.jdk11:jminor-plugin-json:0.14.1'
    //Local JDBC connection module
    runtimeOnly 'org.jminor.jdk11:jminor-framework-db-local:0.14.1'
    //H2 DBMS module
    runtimeOnly 'org.jminor.jdk11:jminor-dbms-h2database:0.14.1'
    //H2 JDBC driver
    runtimeOnly 'com.h2database:h2:1.4.200'
    //Logging with Logback
    runtimeOnly 'org.jminor.jdk11:jminor-plugin-logback-proxy:0.14.1'

    //Domain model unit testing module
    testImplementation 'org.jminor.jdk11:jminor-framework-domain-test:0.14.1'
    //JUnit 5
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0'
}

6. Code examples

There are at minimum two steps involved in creating a JMinor application:

  • Defining a domain model based on the underlying tables.

  • Creating edit panels for tables requiring CRUD functionality.

6.1. Domain

The JMinor framework is based around the Entity class, a map-like structure, representing a row in a table. The domain is modelled much like an E/R diagram, with an EntityDefinition per table, a Property for each column and a ForeignKeyProperty for each foreign key relationship.

Module

Artifact

org.jminor.framework.domain

org.jminor.jdk11:jminor-framework-domain:0.14.1

6.1.1. Defining entities

The below examples are somewhat simplified, but functionally correct.

View example schema SQL
Note
Not all columns are used in the example code below.
create user if not exists scott password 'tiger';
alter user scott admin true;

create schema store;

create table store.customer (
  id varchar(36) not null,
  first_name varchar(40) not null,
  last_name varchar(40) not null,
  email varchar(100),
  is_active boolean default true not null,
  constraint customer_pk primary key (id),
  constraint customer_email_uk unique (email)
);

create table store.address (
  id identity not null,
  street varchar(120) not null,
  city varchar(50) not null,
  valid boolean default true not null,
  constraint address_pk primary key (id),
  constraint address_uk unique (street, city)
);

create table store.customer_address (
  id identity not null,
  customer_id varchar(36) not null,
  address_id integer not null,
  constraint customer_address_pk primary key (id),
  constraint customer_address_uk unique (customer_id, address_id)
);

insert into store.customer(id, first_name, last_name, email)
values ('ff60ebc3-bae0-4c0f-b094-4129edd3665a', 'John', 'Doe', 'doe@doe.net');

insert into store.address(street, city)
values ('Elm Street 123', 'Syracuse');

insert into store.customer_address(customer_id, address_id)
values ('ff60ebc3-bae0-4c0f-b094-4129edd3665a', 1);

commit;

First we define a entity based on the table store.customer, using String constants to identify the entity type (the entityId) and its properties (propertyIds).

Note
In this example the constants contain the actual table and column names, but these can be specified via parameters if that is preferred (see manual).
public static final String T_CUSTOMER = "store.customer";
public static final String CUSTOMER_ID = "id";
public static final String CUSTOMER_FIRST_NAME = "first_name";
public static final String CUSTOMER_LAST_NAME = "last_name";

void customer() {
  define(T_CUSTOMER,
          primaryKeyProperty(CUSTOMER_ID, Types.VARCHAR),
          columnProperty(CUSTOMER_FIRST_NAME, Types.VARCHAR, "First name")
                  .nullable(false).maximumLength(40),
          columnProperty(CUSTOMER_LAST_NAME, Types.VARCHAR, "Last name")
                  .nullable(false).maximumLength(40))
          .keyGenerator(new KeyGenerator() {
            @Override
            public void beforeInsert(Entity entity, EntityDefinition definition,
                                     DatabaseConnection connection) throws SQLException {
              entity.put(CUSTOMER_ID, randomUUID().toString());
            }
          })
          .stringProvider(new StringProvider(CUSTOMER_LAST_NAME)
                  .addText(", ").addValue(CUSTOMER_FIRST_NAME));
}

Next we define a entity based on the table store.address.

public static final String T_ADDRESS = "store.address";
public static final String ADDRESS_ID = "id";
public static final String ADDRESS_STREET = "street";
public static final String ADDRESS_CITY = "city";

void address() {
  define(T_ADDRESS,
          primaryKeyProperty(ADDRESS_ID, Types.INTEGER),
          columnProperty(ADDRESS_STREET, Types.VARCHAR, "Street")
                  .nullable(false).maximumLength(120),
          columnProperty(ADDRESS_CITY, Types.VARCHAR, "City")
                  .nullable(false).maximumLength(50))
          .keyGenerator(automatic(T_ADDRESS))
          .stringProvider(new StringProvider(ADDRESS_STREET)
                  .addText(", ").addValue(ADDRESS_CITY));
}

And finally we define a entity based on the many-to-many relationship table store.customer_address.

public static final String T_CUSTOMER_ADDRESS = "store.customer_address";
public static final String CUSTOMER_ADDRESS_ID = "id";
public static final String CUSTOMER_ADDRESS_CUSTOMER_ID = "customer_id";
public static final String CUSTOMER_ADDRESS_CUSTOMER_FK = "customer_fk";
public static final String CUSTOMER_ADDRESS_ADDRESS_ID = "address_id";
public static final String CUSTOMER_ADDRESS_ADDRESS_FK = "address_fk";

void customerAddress() {
  define(T_CUSTOMER_ADDRESS,
          primaryKeyProperty(CUSTOMER_ADDRESS_ID, Types.INTEGER),
          foreignKeyProperty(CUSTOMER_ADDRESS_CUSTOMER_FK, "Customer", T_CUSTOMER,
                  columnProperty(CUSTOMER_ADDRESS_CUSTOMER_ID, Types.VARCHAR))
                  .nullable(false),
          foreignKeyProperty(CUSTOMER_ADDRESS_ADDRESS_FK, "Address", T_ADDRESS,
                  columnProperty(CUSTOMER_ADDRESS_ADDRESS_ID, Types.INTEGER))
                  .nullable(false))
          .keyGenerator(automatic(T_CUSTOMER_ADDRESS))
          .caption("Customer address");
}

6.2. UI

The EntityPanel class provides a Swing UI for viewing and editing entities. It is composed of a EntityEditPanel and a EntityTablePanel. For each of these panel classes there is a corresponding model class; SwingEntityModel, which is composed of a SwingEntityEditModel and a SwingEntityTableModel. The only class you are required to extend is the EntityEditPanel, which provides the input controls for editing a entity. Below we demonstrate how to set up a simple master/detail panel.

Module

Artifact

org.jminor.swing.framework.ui

org.jminor.jdk11:jminor-swing-framework-ui:0.14.1

6.2.1. Master

Here we extend a EntityEditPanel to provide the UI for editing a customer and use that edit panel class when we assemble the EntityPanel. We use a default SwingEntityModel implementation, which internally, creates a default SwingEntityEditModel and SwingEntityTableModel.

class CustomerEditPanel extends EntityEditPanel {

  public CustomerEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(CUSTOMER_FIRST_NAME);
    createTextField(CUSTOMER_FIRST_NAME).setColumns(12);
    createTextField(CUSTOMER_LAST_NAME).setColumns(12);
    addPropertyPanel(CUSTOMER_FIRST_NAME);
    addPropertyPanel(CUSTOMER_LAST_NAME);
  }
}

EntityConnectionProvider connectionProvider =
        new LocalEntityConnectionProvider(Databases.getInstance())
                .setDomainClassName(Store.class.getName())
                .setUser(Users.parseUser("scott:tiger"));

SwingEntityModel customerModel = new SwingEntityModel(T_CUSTOMER, connectionProvider);

EntityPanel customerPanel = new EntityPanel(customerModel,
        new CustomerEditPanel(customerModel.getEditModel()));

6.2.2. Detail

Here we create a panel for viewing and editing customer addresses, much like the one above. We start by creating a default SwingEntityModel instance, which we add as a detail model on the customer model. Finally, we create a EntityPanel for the customer address and add that as a detail panel on the customer panel.

class CustomerAddressEditPanel extends EntityEditPanel {

  public CustomerAddressEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(CUSTOMER_ADDRESS_CUSTOMER_FK);
    createForeignKeyComboBox(CUSTOMER_ADDRESS_CUSTOMER_FK);
    createForeignKeyComboBox(CUSTOMER_ADDRESS_ADDRESS_FK);
    addPropertyPanel(CUSTOMER_ADDRESS_CUSTOMER_FK);
    addPropertyPanel(CUSTOMER_ADDRESS_ADDRESS_FK);
  }
}

SwingEntityModel customerAddressModel = new SwingEntityModel(T_CUSTOMER_ADDRESS, connectionProvider);

customerModel.addDetailModel(customerAddressModel);

EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
        new CustomerAddressEditPanel(customerAddressModel.getEditModel()));

customerPanel.addDetailPanel(customerAddressPanel);

//lazy initialization of UI components
customerPanel.initializePanel();

//populate the model with data from the database
customerModel.refresh();

Dialogs.displayInDialog(null, customerPanel, "Customers");

6.3. Domain unit test

The EntityTestUnit class provides a way to unit test the domain model. The test method performs insert, update, select and delete on a randomly generated entity instance and verifies the results.

Module

Artifact

org.jminor.framework.domain.test

org.jminor.jdk11:jminor-framework-domain-test:0.14.1

class StoreTest extends EntityTestUnit {

  public StoreTest() {
    super(Store.class.getName(), Users.parseUser("scott:tiger"));
  }

  @Test
  void customer() throws DatabaseException {
    test(T_CUSTOMER);
  }

  @Test
  void address() throws DatabaseException {
    test(T_ADDRESS);
  }

  @Test
  void customerAddress() throws DatabaseException {
    test(T_CUSTOMER_ADDRESS);
  }
}

6.4. Persistance

The EntityConnection interface provides select, insert, update, and delete methods. It has three available implementations, one of which is based on a local JDBC connection, used below.

Module

Artifact

Description

org.jminor.framework.db.core

org.jminor.jdk11:jminor-framework-db-core:0.14.1

Database API

org.jminor.framework.db.local

org.jminor.jdk11:jminor-framework-db-local:0.14.1

Local JDBC

6.4.1. Selecting

Store domain = new Store();

EntityConnection connection =
        LocalEntityConnections.createConnection(
                domain, Databases.getInstance(), Users.parseUser("scott:tiger"));

//select customer where last name = Doe
Entity johnDoe = connection.selectSingle(T_CUSTOMER, CUSTOMER_LAST_NAME, "Doe");

//select all customer addresses
List<Entity> customerAddresses = //where customer = john doe
        connection.select(T_CUSTOMER_ADDRESS, CUSTOMER_ADDRESS_CUSTOMER_FK, johnDoe);

Entity customerAddress = customerAddresses.get(0);

Entity address = customerAddress.getForeignKey(CUSTOMER_ADDRESS_ADDRESS_FK);

String lastName = johnDoe.getString(CUSTOMER_LAST_NAME);
String street = address.getString(ADDRESS_STREET);
String city = address.getString(ADDRESS_CITY);

6.4.2. Persisting

Store domain = new Store();

EntityConnection connection =
        LocalEntityConnections.createConnection(
                domain, Databases.getInstance(), Users.parseUser("scott:tiger"));

Entity customer = domain.entity(T_CUSTOMER);
customer.put(CUSTOMER_FIRST_NAME, "John");
customer.put(CUSTOMER_LAST_NAME, "Doe");

connection.insert(customer);

Entity address = domain.entity(T_ADDRESS);
address.put(ADDRESS_STREET, "Elm Street 321");
address.put(ADDRESS_CITY, "Syracuse");

connection.insert(address);

Entity customerAddress = domain.entity(T_CUSTOMER_ADDRESS);
customerAddress.put(CUSTOMER_ADDRESS_CUSTOMER_FK, customer);
customerAddress.put(CUSTOMER_ADDRESS_ADDRESS_FK, address);

connection.insert(customerAddress);

customer.put(CUSTOMER_FIRST_NAME, "Jonathan");

connection.update(customer);

connection.delete(customerAddress.getKey());
Full code
/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.manual.quickstart;

import org.jminor.common.db.DatabaseConnection;
import org.jminor.common.db.Databases;
import org.jminor.common.db.exception.DatabaseException;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnection;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.db.local.LocalEntityConnectionProvider;
import org.jminor.framework.db.local.LocalEntityConnections;
import org.jminor.framework.domain.Domain;
import org.jminor.framework.domain.entity.Entity;
import org.jminor.framework.domain.entity.EntityDefinition;
import org.jminor.framework.domain.entity.KeyGenerator;
import org.jminor.framework.domain.entity.StringProvider;
import org.jminor.framework.domain.entity.test.EntityTestUnit;
import org.jminor.swing.common.ui.dialog.Dialogs;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.model.SwingEntityModel;
import org.jminor.swing.framework.ui.EntityEditPanel;
import org.jminor.swing.framework.ui.EntityPanel;

import org.junit.jupiter.api.Test;

import java.sql.SQLException;
import java.sql.Types;
import java.util.List;

import static java.util.UUID.randomUUID;
import static org.jminor.framework.demos.manual.quickstart.Example.Store.*;
import static org.jminor.framework.domain.entity.KeyGenerators.automatic;
import static org.jminor.framework.domain.property.Properties.*;

public final class Example {

  public static class Store extends Domain {

    public Store() {
      customer();
      address();
      customerAddress();
    }

    public static final String T_CUSTOMER = "store.customer";
    public static final String CUSTOMER_ID = "id";
    public static final String CUSTOMER_FIRST_NAME = "first_name";
    public static final String CUSTOMER_LAST_NAME = "last_name";

    void customer() {
      define(T_CUSTOMER,
              primaryKeyProperty(CUSTOMER_ID, Types.VARCHAR),
              columnProperty(CUSTOMER_FIRST_NAME, Types.VARCHAR, "First name")
                      .nullable(false).maximumLength(40),
              columnProperty(CUSTOMER_LAST_NAME, Types.VARCHAR, "Last name")
                      .nullable(false).maximumLength(40))
              .keyGenerator(new KeyGenerator() {
                @Override
                public void beforeInsert(Entity entity, EntityDefinition definition,
                                         DatabaseConnection connection) throws SQLException {
                  entity.put(CUSTOMER_ID, randomUUID().toString());
                }
              })
              .stringProvider(new StringProvider(CUSTOMER_LAST_NAME)
                      .addText(", ").addValue(CUSTOMER_FIRST_NAME));
    }
    public static final String T_ADDRESS = "store.address";
    public static final String ADDRESS_ID = "id";
    public static final String ADDRESS_STREET = "street";
    public static final String ADDRESS_CITY = "city";

    void address() {
      define(T_ADDRESS,
              primaryKeyProperty(ADDRESS_ID, Types.INTEGER),
              columnProperty(ADDRESS_STREET, Types.VARCHAR, "Street")
                      .nullable(false).maximumLength(120),
              columnProperty(ADDRESS_CITY, Types.VARCHAR, "City")
                      .nullable(false).maximumLength(50))
              .keyGenerator(automatic(T_ADDRESS))
              .stringProvider(new StringProvider(ADDRESS_STREET)
                      .addText(", ").addValue(ADDRESS_CITY));
    }
    public static final String T_CUSTOMER_ADDRESS = "store.customer_address";
    public static final String CUSTOMER_ADDRESS_ID = "id";
    public static final String CUSTOMER_ADDRESS_CUSTOMER_ID = "customer_id";
    public static final String CUSTOMER_ADDRESS_CUSTOMER_FK = "customer_fk";
    public static final String CUSTOMER_ADDRESS_ADDRESS_ID = "address_id";
    public static final String CUSTOMER_ADDRESS_ADDRESS_FK = "address_fk";

    void customerAddress() {
      define(T_CUSTOMER_ADDRESS,
              primaryKeyProperty(CUSTOMER_ADDRESS_ID, Types.INTEGER),
              foreignKeyProperty(CUSTOMER_ADDRESS_CUSTOMER_FK, "Customer", T_CUSTOMER,
                      columnProperty(CUSTOMER_ADDRESS_CUSTOMER_ID, Types.VARCHAR))
                      .nullable(false),
              foreignKeyProperty(CUSTOMER_ADDRESS_ADDRESS_FK, "Address", T_ADDRESS,
                      columnProperty(CUSTOMER_ADDRESS_ADDRESS_ID, Types.INTEGER))
                      .nullable(false))
              .keyGenerator(automatic(T_CUSTOMER_ADDRESS))
              .caption("Customer address");
    }
  }

  static void customerPanel() {
    class CustomerEditPanel extends EntityEditPanel {

      public CustomerEditPanel(SwingEntityEditModel editModel) {
        super(editModel);
      }

      @Override
      protected void initializeUI() {
        setInitialFocusProperty(CUSTOMER_FIRST_NAME);
        createTextField(CUSTOMER_FIRST_NAME).setColumns(12);
        createTextField(CUSTOMER_LAST_NAME).setColumns(12);
        addPropertyPanel(CUSTOMER_FIRST_NAME);
        addPropertyPanel(CUSTOMER_LAST_NAME);
      }
    }

    EntityConnectionProvider connectionProvider =
            new LocalEntityConnectionProvider(Databases.getInstance())
                    .setDomainClassName(Store.class.getName())
                    .setUser(Users.parseUser("scott:tiger"));

    SwingEntityModel customerModel = new SwingEntityModel(T_CUSTOMER, connectionProvider);

    EntityPanel customerPanel = new EntityPanel(customerModel,
            new CustomerEditPanel(customerModel.getEditModel()));

    class CustomerAddressEditPanel extends EntityEditPanel {

      public CustomerAddressEditPanel(SwingEntityEditModel editModel) {
        super(editModel);
      }

      @Override
      protected void initializeUI() {
        setInitialFocusProperty(CUSTOMER_ADDRESS_CUSTOMER_FK);
        createForeignKeyComboBox(CUSTOMER_ADDRESS_CUSTOMER_FK);
        createForeignKeyComboBox(CUSTOMER_ADDRESS_ADDRESS_FK);
        addPropertyPanel(CUSTOMER_ADDRESS_CUSTOMER_FK);
        addPropertyPanel(CUSTOMER_ADDRESS_ADDRESS_FK);
      }
    }

    SwingEntityModel customerAddressModel = new SwingEntityModel(T_CUSTOMER_ADDRESS, connectionProvider);

    customerModel.addDetailModel(customerAddressModel);

    EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
            new CustomerAddressEditPanel(customerAddressModel.getEditModel()));

    customerPanel.addDetailPanel(customerAddressPanel);

    //lazy initialization of UI components
    customerPanel.initializePanel();

    //populate the model with data from the database
    customerModel.refresh();

    Dialogs.displayInDialog(null, customerPanel, "Customers");
  }

  static void domainModelTest() {
    class StoreTest extends EntityTestUnit {

      public StoreTest() {
        super(Store.class.getName(), Users.parseUser("scott:tiger"));
      }

      @Test
      void customer() throws DatabaseException {
        test(T_CUSTOMER);
      }

      @Test
      void address() throws DatabaseException {
        test(T_ADDRESS);
      }

      @Test
      void customerAddress() throws DatabaseException {
        test(T_CUSTOMER_ADDRESS);
      }
    }
  }

  static void selectEntities() throws DatabaseException {
    Store domain = new Store();

    EntityConnection connection =
            LocalEntityConnections.createConnection(
                    domain, Databases.getInstance(), Users.parseUser("scott:tiger"));

    //select customer where last name = Doe
    Entity johnDoe = connection.selectSingle(T_CUSTOMER, CUSTOMER_LAST_NAME, "Doe");

    //select all customer addresses
    List<Entity> customerAddresses = //where customer = john doe
            connection.select(T_CUSTOMER_ADDRESS, CUSTOMER_ADDRESS_CUSTOMER_FK, johnDoe);

    Entity customerAddress = customerAddresses.get(0);

    Entity address = customerAddress.getForeignKey(CUSTOMER_ADDRESS_ADDRESS_FK);

    String lastName = johnDoe.getString(CUSTOMER_LAST_NAME);
    String street = address.getString(ADDRESS_STREET);
    String city = address.getString(ADDRESS_CITY);
  }

  static void persistEntities() throws DatabaseException {
    Store domain = new Store();

    EntityConnection connection =
            LocalEntityConnections.createConnection(
                    domain, Databases.getInstance(), Users.parseUser("scott:tiger"));

    Entity customer = domain.entity(T_CUSTOMER);
    customer.put(CUSTOMER_FIRST_NAME, "John");
    customer.put(CUSTOMER_LAST_NAME, "Doe");

    connection.insert(customer);

    Entity address = domain.entity(T_ADDRESS);
    address.put(ADDRESS_STREET, "Elm Street 321");
    address.put(ADDRESS_CITY, "Syracuse");

    connection.insert(address);

    Entity customerAddress = domain.entity(T_CUSTOMER_ADDRESS);
    customerAddress.put(CUSTOMER_ADDRESS_CUSTOMER_FK, customer);
    customerAddress.put(CUSTOMER_ADDRESS_ADDRESS_FK, address);

    connection.insert(customerAddress);

    customer.put(CUSTOMER_FIRST_NAME, "Jonathan");

    connection.update(customer);

    connection.delete(customerAddress.getKey());
  }
}