1. Domain

We start by creating a class named EmpDept in a package of our choosing, extending Domain and define the constants required for the department Entity, which is based on the SCOTT.DEPT table. One constant defines the entity itself and will be referred to as the entityId (here prefixed with T_), the other three represent the columns in the table and will be referred to as propertyIds. The entityId in this example contains the actual table name (scott.dept), but the table name can be specified via a tableName parameter when the entity is defined. The propertyIds contain the actual column names from the table, but that is not required either.

/**
 * This class contains the specification for the EmpDept application domain model
 */
public final class EmpDept extends Domain {

  /**Entity identifier for the table scott.dept*/
  public static final String T_DEPARTMENT = "scott.dept";

  /**Property identifiers for the columns in the scott.dept table*/
  public static final String DEPARTMENT_ID = "deptno";
  public static final String DEPARTMENT_NAME = "dname";
  public static final String DEPARTMENT_LOCATION = "loc";

Next we define the constants required for the employee entity, which is based on the SCOTT.EMP table. Here there are two additional propertyIds with _FK suffixes, we’ll use these later when we specify the foreign key properties for the employee entity.

  /**Entity identifier for the table scott.emp*/
  public static final String T_EMPLOYEE = "scott.emp";

  /**Property identifiers for the columns in the scott.emp table*/
  public static final String EMPLOYEE_ID = "empno";
  public static final String EMPLOYEE_NAME = "ename";
  public static final String EMPLOYEE_JOB = "job";
  public static final String EMPLOYEE_MGR = "mgr";
  public static final String EMPLOYEE_HIREDATE = "hiredate";
  public static final String EMPLOYEE_SALARY = "sal";
  public static final String EMPLOYEE_COMMISSION = "comm";
  public static final String EMPLOYEE_DEPARTMENT = "deptno";
  /**Foreign key (reference) identifier for the DEPT column in the table scott.emp*/
  public static final String EMPLOYEE_DEPARTMENT_FK = "dept_fk";
  /**Foreign key (reference) identifier for the MGR column in the table scott.emp*/
  public static final String EMPLOYEE_MGR_FK = "mgr_fk";
  /**Property identifier for the denormalized department location property*/
  public static final String EMPLOYEE_DEPARTMENT_LOCATION = "location";

  public static final List<Item> JOB_VALUES = asList(
          item("ANALYST", "Analyst"), item("CLERK", "Clerk"),
          item("MANAGER", "Manager"), item("PRESIDENT", "President"),
          item("SALESMAN", "Salesman"));

In this section we add a constructor, calling the two methods which define the domain entities

  /** Initializes this domain model */
  public EmpDept() {
    department();
    employee();
  }

Next we add the method defining the department entity. Entity.Definition.Builder instances are provided by the Domain class, via the define method which comes in two flavours, one which assumes the entityId contains the underlying table name and one which does not, and therefore requires an additional tableName parameter. We use the former below. For an overview of the Property class see Manual#Property. The Entity.Definition.Builder class provides chained setters for setting entity attributes, such as the primary key value source. Here we set idSource to IdSource.NONE since the department number is not generated but rather set manually, the so-called stringProvider which is responsible for providing toString() implementations for entities, the smallDataset attribute which hints that it is OK to automatically base ComboBoxes on the entity and we also set the caption. In this case we simply have the department string provider return the department name.

  void department() {
    /*Defining the entity type T_DEPARTMENT*/
    define(T_DEPARTMENT,
            primaryKeyProperty(DEPARTMENT_ID, Types.INTEGER, "Department no.")
                    .updatable(true).nullable(false),
            columnProperty(DEPARTMENT_NAME, Types.VARCHAR, "Department name")
                    .preferredColumnWidth(120).maximumLength(14).nullable(false),
            columnProperty(DEPARTMENT_LOCATION, Types.VARCHAR, "Location")
                    .preferredColumnWidth(150).maximumLength(13))
            .smallDataset(true)
            .orderBy(orderBy().ascending(DEPARTMENT_NAME))
            .stringProvider(new StringProvider(DEPARTMENT_NAME))
            .caption("Departments");
  }

Next we define the employee entity. Here we set the keyGenerator to Entities.incrementKeyGenerator(T_EMPLOYEE, EMPLOYEE_ID) which, as the name suggests, simply increments the maximum column value by one (N.B. this is a very simplistic implementation which is absolutely not transaction safe). Here we also introduce the ForeignKeyProperty, the orderBy, as well as a ColorProvider which is responsible for providing a custom color for a entity instance.

  void employee() {
    /*Defining the entity type T_EMPLOYEE*/
    define(T_EMPLOYEE,
            primaryKeyProperty(EMPLOYEE_ID, Types.INTEGER, "Employee no."),
            columnProperty(EMPLOYEE_NAME, Types.VARCHAR, "Name")
                    .maximumLength(10).nullable(false),
            foreignKeyProperty(EMPLOYEE_DEPARTMENT_FK, "Department", T_DEPARTMENT,
                    columnProperty(EMPLOYEE_DEPARTMENT))
                    .nullable(false),
            valueListProperty(EMPLOYEE_JOB, Types.VARCHAR, "Job", JOB_VALUES),
            columnProperty(EMPLOYEE_SALARY, Types.DECIMAL, "Salary")
                    .nullable(false).minimumValue(1000).maximumValue(10000).maximumFractionDigits(2),
            columnProperty(EMPLOYEE_COMMISSION, Types.DOUBLE, "Commission")
                    .minimumValue(100).maximumValue(2000).maximumFractionDigits(2),
            foreignKeyProperty(EMPLOYEE_MGR_FK, "Manager", T_EMPLOYEE,
                    columnProperty(EMPLOYEE_MGR)),
            columnProperty(EMPLOYEE_HIREDATE, Types.DATE, "Hiredate")
                    .nullable(false),
            denormalizedViewProperty(EMPLOYEE_DEPARTMENT_LOCATION, EMPLOYEE_DEPARTMENT_FK,
                    getDefinition(T_DEPARTMENT).getProperty(DEPARTMENT_LOCATION), "Location")
                    .preferredColumnWidth(100))
            .keyGenerator(increment(T_EMPLOYEE, EMPLOYEE_ID))
            .orderBy(orderBy().ascending(EMPLOYEE_DEPARTMENT, EMPLOYEE_NAME))
            .searchPropertyIds(EMPLOYEE_NAME)
            .stringProvider(new StringProvider(EMPLOYEE_NAME))
            .caption("Employee")
            .colorProvider((entity, property) -> {
              if (property.is(EMPLOYEE_JOB) && "MANAGER".equals(entity.get(EMPLOYEE_JOB))) {
                return Color.CYAN;
              }

              return null;
            });
  }
}

1.1. Domain unit test

To unit test the the domain model we extend EntityTestUnit and create a public test method for each entity we want tested and within that method call test(entityId). The EntityTestUnit relies on information from the domain model to construct random entity instances and run insert, update, select and delete tests. The tests are run within their own transactions which are then rolled back. We can provide our own entity instances if we’d like by overriding initializeTestEntity(entityId, foreignKeyEntities) handling the cases we’d like and delegating the rest to the superclass implementation. The same can be done for entities that are referenced via foreign keys and are required in order to be able to insert records. We override getTestUser(), the default implementation simply prompts for login credentials.

public class EmpDeptTest extends EntityTestUnit {

  public EmpDeptTest() {
    super(EmpDept.class.getName());
  }

  @Test
  public void department() throws Exception {
    test(T_DEPARTMENT);
  }

  @Test
  public void employee() throws Exception {
    test(T_EMPLOYEE);
  }
}

Here are the parameters required for running the EmpDept test on the default H2 embedded database, assuming the database resides in the working directory.

-Djminor.db.type=h2

-Djminor.db.embedded=true

-Djminor.db.host=h2db/h2

For further information on parameters required for running JMinor applications see client configuration and server configuration.

2. Model

In many cases we can use the default model implementations, but here we will customize the employee edit model by extending SwingEntityEditModel.

We must provide a constructor with a single EntityConnectionProvider parameter. We also call the bindEvents method we define later on.

public final class EmployeeEditModel extends SwingEntityEditModel {

  public EmployeeEditModel(final EntityConnectionProvider connectionProvider) {
    super(EmpDept.T_EMPLOYEE, connectionProvider);
    bindEvents();
  }

At some point we are going to be editing the manager property of an employee which means we’ll need a value list of managers. We override createForeignKeyComboBoxModel in order to provide a specialized combo box model for the manager property.

Note
The foreign key propertyId (EmpDept.EMPLOYEE_MGR_FK) is used when referring to the manager property, very rarely do we have to worry about the underlying reference property (EmpDept.EMPLOYEE_MGR).

We start by calling the super class method which creates an unfiltered EntityComboBoxModel, then we configure the one for the manager foreign key so it only display employees with the job PRESIDENT or MANAGER.

  /** Providing a custom ComboBoxModel for the manager property, which only shows managers and the president */
  @Override
  public SwingEntityComboBoxModel createForeignKeyComboBoxModel(final ForeignKeyProperty foreignKeyProperty) {
    final SwingEntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKeyProperty);
    if (foreignKeyProperty.is(EmpDept.EMPLOYEE_MGR_FK)) {
      //Customize the null value so that it displays the chosen
      //text instead of the default '-' character
      comboBoxModel.setNullValue(getDomain().createToStringEntity(EmpDept.T_EMPLOYEE, "None"));
      //we do not want filtering to remove a value that is selected
      //and thereby change the selection, see bindEvents() below
      comboBoxModel.setFilterSelectedItem(false);
      //Only select the president and managers from the database
      comboBoxModel.setSelectConditionProvider(() ->
              Conditions.propertyCondition(EmpDept.EMPLOYEE_JOB, ConditionType.LIKE, asList("MANAGER", "PRESIDENT")));
    }

    return comboBoxModel;
  }

Finally we bind a couple of events, for further information see Manual#Event binding.

  private void bindEvents() {
    //Refresh the manager ComboBoxModel when an employee is added, deleted or updated,
    //in case a new manager got hired, fired or promoted
    addEntitiesChangedListener(() -> getForeignKeyComboBoxModel(EmpDept.EMPLOYEE_MGR_FK).refresh());
    //Filter the manager ComboBoxModel so that only managers from the selected department are shown,
    //this filtering happens each time the department value is changed, either when an employee is
    //selected or the department combo box selection changes
    addValueListener(EmpDept.EMPLOYEE_DEPARTMENT_FK, valueChange -> {
      //only show managers from the same department as the selected employee and hide the currently
      //selected employee to prevent an employee from being made her own manager
      getForeignKeyComboBoxModel(EmpDept.EMPLOYEE_MGR_FK).setIncludeCondition(manager ->
              Objects.equals(manager.getForeignKey(EmpDept.EMPLOYEE_DEPARTMENT_FK), valueChange.getValue())
                      && !Objects.equals(manager, getEntity()));
    });
  }
}

3. UI

If we want to do any editing we must provide a EntityEditPanel implementation for the entity, we start by creating a DepartmentEditPanel.

A class extending EntityEditPanel must provide a constructor taking a single SwingEntityEditModel parameter.

public class DepartmentEditPanel extends EntityEditPanel {

  public DepartmentEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

We override the intializeUI method to construct the actual UI, used for editing the department entity. The EntityEditPanel class provides methods for creating all the basic controls required, named create…​, such as createTextField(propertyId) or createForeignKeyComboBox(foreignKeyPropertyId).

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(EmpDept.DEPARTMENT_ID);

    final JTextField departmentIdField = createTextField(EmpDept.DEPARTMENT_ID);
    departmentIdField.setColumns(10);
    TextFields.makeUpperCase(createTextField(EmpDept.DEPARTMENT_NAME));
    TextFields.makeUpperCase(createTextField(EmpDept.DEPARTMENT_LOCATION));

    //we don't allow editing of the department number since it's a primary key
    getEditModel().getPrimaryKeyNullObserver().addListener(() -> {
      if (getEditModel().isEntityNew()) {
        departmentIdField.setEnabled(true);
        setInitialFocusProperty(EmpDept.DEPARTMENT_ID);
      }
      else {
        departmentIdField.setEnabled(false);
        setInitialFocusProperty(EmpDept.DEPARTMENT_NAME);
      }
    });

    setLayout(new GridLayout(3, 1, 5, 5));

    addPropertyPanel(EmpDept.DEPARTMENT_ID);
    addPropertyPanel(EmpDept.DEPARTMENT_NAME);
    addPropertyPanel(EmpDept.DEPARTMENT_LOCATION);
  }
}

We extend EntityTablePanel for the department entity in order to provide a report print action.

A class extending EntityTablePanel must provide a constructor taking a single SwingEntityTableModel parameter.

public class DepartmentTablePanel extends EntityTablePanel {

  public DepartmentTablePanel(final SwingEntityTableModel tableModel) {
    super(tableModel);
  }

We create a method for viewing a report, which is called via an action we’ll initialize in the next step. For further information about report viewing and printing see Manual#Reporting with JasperReports.

  public void viewEmployeeReport() throws Exception {
    final String reportPath = ReportWrapper.getReportPath() + "/empdept_employees.jasper";
    final Collection<Integer> departmentNumbers =
            Entities.getDistinctValues(EmpDept.DEPARTMENT_ID,
                    getTableModel().getSelectionModel().getSelectedItems());
    final HashMap<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("DEPTNO", departmentNumbers);
    EntityReportUiUtil.viewJdbcReport(DepartmentTablePanel.this,
            new JasperReportsWrapper(reportPath, reportParameters),
            new JasperReportsUIWrapper(), "Employee Report", getTableModel().getConnectionProvider());
  }

Next we override getPrintControls() to add our report action to the popup print control set.

  @Override
  protected ControlSet getPrintControls() {
    final ControlSet printControlSet = super.getPrintControls();
    final StateObserver selectionNotEmptyObserver =
            getTableModel().getSelectionModel().getSelectionNotEmptyObserver();
    printControlSet.add(Controls.control(this::viewEmployeeReport,
            "Employee Report", selectionNotEmptyObserver));

    return printControlSet;
  }
}

For editing employee entities we create the EmployeeEditPanel class.

public class EmployeeEditPanel extends EntityEditPanel {

  public EmployeeEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

All we have to do is override initializeUI.

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(EmpDept.EMPLOYEE_NAME);

    final JTextField nameField = TextFields.makeUpperCase(createTextField(EmpDept.EMPLOYEE_NAME));
    nameField.setColumns(8);
    createValueListComboBox(EmpDept.EMPLOYEE_JOB);
    final JComboBox managerBox = createForeignKeyComboBox(EmpDept.EMPLOYEE_MGR_FK);
    managerBox.setPreferredSize(TextFields.getPreferredTextFieldSize());
    createForeignKeyComboBox(EmpDept.EMPLOYEE_DEPARTMENT_FK);
    createTextField(EmpDept.EMPLOYEE_SALARY);
    createTextField(EmpDept.EMPLOYEE_COMMISSION);
    createTemporalInputPanel(EmpDept.EMPLOYEE_HIREDATE, true);

    setLayout(new FlexibleGridLayout(3, 3, 5, 5, true, false));

    addPropertyPanel(EmpDept.EMPLOYEE_NAME);
    addPropertyPanel(EmpDept.EMPLOYEE_JOB);
    addPropertyPanel(EmpDept.EMPLOYEE_DEPARTMENT_FK);

    addPropertyPanel(EmpDept.EMPLOYEE_MGR_FK);
    addPropertyPanel(EmpDept.EMPLOYEE_SALARY);
    addPropertyPanel(EmpDept.EMPLOYEE_COMMISSION);

    addPropertyPanel(EmpDept.EMPLOYEE_HIREDATE);
    add(new JLabel());
    add(new JLabel());
  }
}

3.1. Main application panel

We create a main application panel by extending EntityApplicationPanel. Overriding setupEntityPanelBuilders() we configure two sets of SwingEntityModelBuilder and EntityPanelBuilder objects, which allow lazy initialization of UI components. We’ll define the EmployeePanelBuilder class later. We call addEntityPanelBuilder() making the department panel the main application panel.

public class EmpDeptAppPanel extends EntityApplicationPanel<EmpDeptAppPanel.EmpDeptApplicationModel> {

  @Override
  protected void setupEntityPanelBuilders() {
    final EmployeeModelBuilder employeeModelBuilder = new EmployeeModelBuilder();
    final EmployeePanelBuilder employeePanelBuilder =
            new EmployeePanelBuilder(employeeModelBuilder);
    employeePanelBuilder.setEditPanelClass(EmployeeEditPanel.class);

    final SwingEntityModelBuilder departmentModelBuilder = new SwingEntityModelBuilder(EmpDept.T_DEPARTMENT) {
      @Override
      protected void configureModel(final SwingEntityModel entityModel) {
        entityModel.getDetailModel(EmpDept.T_EMPLOYEE).getTableModel().getQueryConditionRequiredState().set(false);
      }
    };
    //This relies on the foreign key association between employee and department
    departmentModelBuilder.addDetailModelBuilder(employeeModelBuilder);

    final EntityPanelBuilder departmentPanelBuilder =
            new EntityPanelBuilder(departmentModelBuilder);
    departmentPanelBuilder.setEditPanelClass(DepartmentEditPanel.class);
    departmentPanelBuilder.setTablePanelClass(DepartmentTablePanel.class);
    departmentPanelBuilder.addDetailPanelBuilder(employeePanelBuilder);

    addEntityPanelBuilder(departmentPanelBuilder);
  }

Next we add a method for importing a JSON text file, which we’ll call via an action initialized in the next section.

  public void importJSON() throws Exception {
    final File file = Dialogs.selectFile(this, null);
    Dialogs.displayInDialog(this, EntityTablePanel.createReadOnlyEntityTablePanel(
            new EntityJSONParser(getModel().getDomain()).deserializeEntities(
                    Text.getTextFileContents(file.getAbsolutePath(), Charset.defaultCharset())), getModel().getConnectionProvider()), "Import");
  }

We override getToolsControlSet() to add our import action to the Tools menu.

  @Override
  protected ControlSet getToolsControlSet() {
    final ControlSet toolsSet = super.getToolsControlSet();
    toolsSet.add(Controls.control(this::importJSON, "Import JSON"));

    return toolsSet;
  }

We implement initializeApplicationModel() by returning an instance of the EmpDeptApplicationModel class, which we’ll define later.

  @Override
  protected EmpDeptApplicationModel initializeApplicationModel(final EntityConnectionProvider connectionProvider) throws CancelException {
    return new EmpDeptApplicationModel(connectionProvider);
  }

We create a main() method for configuring and running the application. See client configuration for what configuration is required for running the client.

  public static void main(final String[] args) {
    EntityEditModel.POST_EDIT_EVENTS.set(true);
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    EntityPanel.COMPACT_ENTITY_PANEL_LAYOUT.set(true);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("org.jminor.framework.demos.empdept.domain.EmpDept");
    new EmpDeptAppPanel().startApplication("Emp-Dept", null, false,
            Windows.getScreenSizeRatio(0.6), Users.parseUser("scott:tiger"));
  }

We then define the EmpDeptApplicationModel class by extending SwingEntityApplicationModel.

  public static final class EmpDeptApplicationModel extends SwingEntityApplicationModel {

    public EmpDeptApplicationModel(final EntityConnectionProvider connectionProvider) {
      super(connectionProvider);
    }
  }

Then we define a EmployeeModelBuilder by extending SwingEntityModelBuilder, in order to configure the table model after initialization.

  private static final class EmployeeModelBuilder extends SwingEntityModelBuilder {
    private EmployeeModelBuilder() {
      super(EmpDept.T_EMPLOYEE);
      setEditModelClass(EmployeeEditModel.class);
    }

    @Override
    protected void configureTableModel(final SwingEntityTableModel tableModel) {
      tableModel.getColumnSummaryModel(EmpDept.EMPLOYEE_SALARY).setSummary(ColumnSummary.AVERAGE);
    }
  }

We create a EmployeePanelBuilder by extending EntityPanelBuilder, in order to configure the table panel after initialization.

  private static final class EmployeePanelBuilder extends EntityPanelBuilder {
    private EmployeePanelBuilder(final EmployeeModelBuilder modelProvider) {
      super(modelProvider);
    }

    @Override
    protected void configureTablePanel(final EntityTablePanel tablePanel) {
      tablePanel.setSummaryPanelVisible(true);
    }
  }
}

4. Load test

public final class EmpDeptLoadTest extends EntityLoadTestModel {

  private static final User UNIT_TEST_USER =
          Users.parseUser(System.getProperty("jminor.test.user", "scott:tiger"));

  public EmpDeptLoadTest() {
    super(UNIT_TEST_USER, asList(new InsertDepartment(), new InsertEmployee(), new LoginLogout(),
            new SelectDepartment(), new UpdateEmployee()));
  }

  @Override
  protected EntityApplicationModel initializeApplication() throws CancelException {
    final EntityApplicationModel applicationModel = new EmpDeptAppPanel.EmpDeptApplicationModel(
            EntityConnectionProviders.connectionProvider().setDomainClassName(EmpDept.class.getName())
                    .setClientTypeId(EmpDeptLoadTest.class.getSimpleName())
                    .setUser(getUser()));
    final EntityModel deptModel = new SwingEntityModel(EmpDept.T_DEPARTMENT, applicationModel.getConnectionProvider());
    deptModel.addDetailModel(new SwingEntityModel(EmpDept.T_EMPLOYEE, applicationModel.getConnectionProvider()));
    applicationModel.addEntityModel(deptModel);

    final EntityModel model = applicationModel.getEntityModel(EmpDept.T_DEPARTMENT);
    model.addLinkedDetailModel(model.getDetailModel(EmpDept.T_EMPLOYEE));
    try {
      model.refresh();
    }
    catch (final Exception ignored) {/*ignored*/}

    return applicationModel;
  }

  private static final class SelectDepartment extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {
    @Override
    protected void performScenario(final EmpDeptAppPanel.EmpDeptApplicationModel application) {
      selectRandomRow(application.getEntityModel(EmpDept.T_DEPARTMENT).getTableModel());
    }
    @Override
    public int getDefaultWeight() {
      return 10;
    }
  }

  private static final class UpdateEmployee extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {

    private final Random random = new Random();

    @Override
    protected void performScenario(final EmpDeptAppPanel.EmpDeptApplicationModel application) throws ScenarioException {
      try {
        final SwingEntityModel departmentModel = application.getEntityModel(EmpDept.T_DEPARTMENT);
        selectRandomRow(departmentModel.getTableModel());
        final SwingEntityModel employeeModel = departmentModel.getDetailModel(EmpDept.T_EMPLOYEE);
        if (employeeModel.getTableModel().getRowCount() > 0) {
          employeeModel.getConnectionProvider().getConnection().beginTransaction();
          try {
            selectRandomRow(employeeModel.getTableModel());
            Entity selected = employeeModel.getTableModel().getSelectionModel().getSelectedItem();
            EntityTestUnit.randomize(application.getDomain(), selected, null);
            employeeModel.getEditModel().setEntity(selected);
            employeeModel.getEditModel().update();
            selectRandomRow(employeeModel.getTableModel());
            selected = employeeModel.getTableModel().getSelectionModel().getSelectedItem();
            EntityTestUnit.randomize(application.getDomain(), selected, null);
            employeeModel.getEditModel().setEntity(selected);
            employeeModel.getEditModel().update();
          }
          finally {
            if (random.nextBoolean()) {
              employeeModel.getConnectionProvider().getConnection().rollbackTransaction();
            }
            else {
              employeeModel.getConnectionProvider().getConnection().commitTransaction();
            }
          }
        }
      }
      catch (final Exception e) {
        throw new ScenarioException(e);
      }
    }
    @Override
    public int getDefaultWeight() {
      return 5;
    }
  }

  private static final class InsertEmployee extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {
    @Override
    protected void performScenario(final EmpDeptAppPanel.EmpDeptApplicationModel application) throws ScenarioException {
      try {
        final SwingEntityModel departmentModel = application.getEntityModel(EmpDept.T_DEPARTMENT);
        selectRandomRow(departmentModel.getTableModel());
        final SwingEntityModel employeeModel = departmentModel.getDetailModel(EmpDept.T_EMPLOYEE);
        final Map<String, Entity> references = new HashMap<>();
        references.put(EmpDept.T_DEPARTMENT, departmentModel.getTableModel().getSelectionModel().getSelectedItem());
        employeeModel.getEditModel().setEntity(EntityTestUnit.createRandomEntity(application.getDomain(), EmpDept.T_EMPLOYEE, references));
        employeeModel.getEditModel().insert();
      }
      catch (final Exception e) {
        throw new ScenarioException(e);
      }
    }
    @Override
    public int getDefaultWeight() {
      return 3;
    }
  }

  private static final class InsertDepartment extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {
    @Override
    protected void performScenario(final EmpDeptAppPanel.EmpDeptApplicationModel application) throws ScenarioException {
      try {
        final SwingEntityModel departmentModel = application.getEntityModel(EmpDept.T_DEPARTMENT);
        departmentModel.getEditModel().setEntity(EntityTestUnit.createRandomEntity(application.getDomain(), EmpDept.T_DEPARTMENT, null));
        departmentModel.getEditModel().insert();
      }
      catch (final Exception e) {
        throw new ScenarioException(e);
      }
    }
  }

  private static final class LoginLogout extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {
    final Random random = new Random();
    @Override
    protected void performScenario(final EmpDeptAppPanel.EmpDeptApplicationModel application) {
      try {
        application.getConnectionProvider().disconnect();
        Thread.sleep(random.nextInt(1500));
        application.getConnectionProvider().getConnection();
      }
      catch (final InterruptedException ignored) {/*ignored*/}
    }
    @Override
    public int getDefaultWeight() {
      return 4;
    }
  }

  public static void main(final String[] args) throws Exception {
    SwingUtilities.invokeLater(new Runner());
  }

  private static final class Runner implements Runnable {
    @Override
    public void run() {
      try {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        new LoadTestPanel(new EmpDeptLoadTest()).showFrame();
      }
      catch (final Exception e) {
        e.printStackTrace();
      }
    }
  }
}

5. Full Demo Source

5.1. Domain

/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.domain;

import org.jminor.common.item.Item;
import org.jminor.framework.domain.Domain;
import org.jminor.framework.domain.entity.StringProvider;

import java.awt.Color;
import java.sql.Types;
import java.util.List;

import static java.util.Arrays.asList;
import static org.jminor.common.item.Items.item;
import static org.jminor.framework.domain.entity.KeyGenerators.increment;
import static org.jminor.framework.domain.entity.OrderBy.orderBy;
import static org.jminor.framework.domain.property.Properties.*;

/**
 * This class contains the specification for the EmpDept application domain model
 */
public final class EmpDept extends Domain {

  /**Entity identifier for the table scott.dept*/
  public static final String T_DEPARTMENT = "scott.dept";

  /**Property identifiers for the columns in the scott.dept table*/
  public static final String DEPARTMENT_ID = "deptno";
  public static final String DEPARTMENT_NAME = "dname";
  public static final String DEPARTMENT_LOCATION = "loc";

  /**Entity identifier for the table scott.emp*/
  public static final String T_EMPLOYEE = "scott.emp";

  /**Property identifiers for the columns in the scott.emp table*/
  public static final String EMPLOYEE_ID = "empno";
  public static final String EMPLOYEE_NAME = "ename";
  public static final String EMPLOYEE_JOB = "job";
  public static final String EMPLOYEE_MGR = "mgr";
  public static final String EMPLOYEE_HIREDATE = "hiredate";
  public static final String EMPLOYEE_SALARY = "sal";
  public static final String EMPLOYEE_COMMISSION = "comm";
  public static final String EMPLOYEE_DEPARTMENT = "deptno";
  /**Foreign key (reference) identifier for the DEPT column in the table scott.emp*/
  public static final String EMPLOYEE_DEPARTMENT_FK = "dept_fk";
  /**Foreign key (reference) identifier for the MGR column in the table scott.emp*/
  public static final String EMPLOYEE_MGR_FK = "mgr_fk";
  /**Property identifier for the denormalized department location property*/
  public static final String EMPLOYEE_DEPARTMENT_LOCATION = "location";

  public static final List<Item> JOB_VALUES = asList(
          item("ANALYST", "Analyst"), item("CLERK", "Clerk"),
          item("MANAGER", "Manager"), item("PRESIDENT", "President"),
          item("SALESMAN", "Salesman"));

  /** Initializes this domain model */
  public EmpDept() {
    department();
    employee();
  }

  void department() {
    /*Defining the entity type T_DEPARTMENT*/
    define(T_DEPARTMENT,
            primaryKeyProperty(DEPARTMENT_ID, Types.INTEGER, "Department no.")
                    .updatable(true).nullable(false),
            columnProperty(DEPARTMENT_NAME, Types.VARCHAR, "Department name")
                    .preferredColumnWidth(120).maximumLength(14).nullable(false),
            columnProperty(DEPARTMENT_LOCATION, Types.VARCHAR, "Location")
                    .preferredColumnWidth(150).maximumLength(13))
            .smallDataset(true)
            .orderBy(orderBy().ascending(DEPARTMENT_NAME))
            .stringProvider(new StringProvider(DEPARTMENT_NAME))
            .caption("Departments");
  }

  void employee() {
    /*Defining the entity type T_EMPLOYEE*/
    define(T_EMPLOYEE,
            primaryKeyProperty(EMPLOYEE_ID, Types.INTEGER, "Employee no."),
            columnProperty(EMPLOYEE_NAME, Types.VARCHAR, "Name")
                    .maximumLength(10).nullable(false),
            foreignKeyProperty(EMPLOYEE_DEPARTMENT_FK, "Department", T_DEPARTMENT,
                    columnProperty(EMPLOYEE_DEPARTMENT))
                    .nullable(false),
            valueListProperty(EMPLOYEE_JOB, Types.VARCHAR, "Job", JOB_VALUES),
            columnProperty(EMPLOYEE_SALARY, Types.DECIMAL, "Salary")
                    .nullable(false).minimumValue(1000).maximumValue(10000).maximumFractionDigits(2),
            columnProperty(EMPLOYEE_COMMISSION, Types.DOUBLE, "Commission")
                    .minimumValue(100).maximumValue(2000).maximumFractionDigits(2),
            foreignKeyProperty(EMPLOYEE_MGR_FK, "Manager", T_EMPLOYEE,
                    columnProperty(EMPLOYEE_MGR)),
            columnProperty(EMPLOYEE_HIREDATE, Types.DATE, "Hiredate")
                    .nullable(false),
            denormalizedViewProperty(EMPLOYEE_DEPARTMENT_LOCATION, EMPLOYEE_DEPARTMENT_FK,
                    getDefinition(T_DEPARTMENT).getProperty(DEPARTMENT_LOCATION), "Location")
                    .preferredColumnWidth(100))
            .keyGenerator(increment(T_EMPLOYEE, EMPLOYEE_ID))
            .orderBy(orderBy().ascending(EMPLOYEE_DEPARTMENT, EMPLOYEE_NAME))
            .searchPropertyIds(EMPLOYEE_NAME)
            .stringProvider(new StringProvider(EMPLOYEE_NAME))
            .caption("Employee")
            .colorProvider((entity, property) -> {
              if (property.is(EMPLOYEE_JOB) && "MANAGER".equals(entity.get(EMPLOYEE_JOB))) {
                return Color.CYAN;
              }

              return null;
            });
  }
}

5.2. Domain unit test

/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.domain;

import org.jminor.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

import static org.jminor.framework.demos.empdept.domain.EmpDept.T_DEPARTMENT;
import static org.jminor.framework.demos.empdept.domain.EmpDept.T_EMPLOYEE;

public class EmpDeptTest extends EntityTestUnit {

  public EmpDeptTest() {
    super(EmpDept.class.getName());
  }

  @Test
  public void department() throws Exception {
    test(T_DEPARTMENT);
  }

  @Test
  public void employee() throws Exception {
    test(T_EMPLOYEE);
  }
}

5.3. Model

/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.model;

import org.jminor.common.db.ConditionType;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.db.condition.Conditions;
import org.jminor.framework.demos.empdept.domain.EmpDept;
import org.jminor.framework.domain.property.ForeignKeyProperty;
import org.jminor.swing.framework.model.SwingEntityComboBoxModel;
import org.jminor.swing.framework.model.SwingEntityEditModel;

import java.util.Objects;

import static java.util.Arrays.asList;

public final class EmployeeEditModel extends SwingEntityEditModel {

  public EmployeeEditModel(final EntityConnectionProvider connectionProvider) {
    super(EmpDept.T_EMPLOYEE, connectionProvider);
    bindEvents();
  }

  /** Providing a custom ComboBoxModel for the manager property, which only shows managers and the president */
  @Override
  public SwingEntityComboBoxModel createForeignKeyComboBoxModel(final ForeignKeyProperty foreignKeyProperty) {
    final SwingEntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKeyProperty);
    if (foreignKeyProperty.is(EmpDept.EMPLOYEE_MGR_FK)) {
      //Customize the null value so that it displays the chosen
      //text instead of the default '-' character
      comboBoxModel.setNullValue(getDomain().createToStringEntity(EmpDept.T_EMPLOYEE, "None"));
      //we do not want filtering to remove a value that is selected
      //and thereby change the selection, see bindEvents() below
      comboBoxModel.setFilterSelectedItem(false);
      //Only select the president and managers from the database
      comboBoxModel.setSelectConditionProvider(() ->
              Conditions.propertyCondition(EmpDept.EMPLOYEE_JOB, ConditionType.LIKE, asList("MANAGER", "PRESIDENT")));
    }

    return comboBoxModel;
  }

  private void bindEvents() {
    //Refresh the manager ComboBoxModel when an employee is added, deleted or updated,
    //in case a new manager got hired, fired or promoted
    addEntitiesChangedListener(() -> getForeignKeyComboBoxModel(EmpDept.EMPLOYEE_MGR_FK).refresh());
    //Filter the manager ComboBoxModel so that only managers from the selected department are shown,
    //this filtering happens each time the department value is changed, either when an employee is
    //selected or the department combo box selection changes
    addValueListener(EmpDept.EMPLOYEE_DEPARTMENT_FK, valueChange -> {
      //only show managers from the same department as the selected employee and hide the currently
      //selected employee to prevent an employee from being made her own manager
      getForeignKeyComboBoxModel(EmpDept.EMPLOYEE_MGR_FK).setIncludeCondition(manager ->
              Objects.equals(manager.getForeignKey(EmpDept.EMPLOYEE_DEPARTMENT_FK), valueChange.getValue())
                      && !Objects.equals(manager, getEntity()));
    });
  }
}

5.4. UI

/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.ui;

import org.jminor.framework.demos.empdept.domain.EmpDept;
import org.jminor.swing.common.ui.textfield.TextFields;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import javax.swing.JTextField;
import java.awt.GridLayout;

public class DepartmentEditPanel extends EntityEditPanel {

  public DepartmentEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(EmpDept.DEPARTMENT_ID);

    final JTextField departmentIdField = createTextField(EmpDept.DEPARTMENT_ID);
    departmentIdField.setColumns(10);
    TextFields.makeUpperCase(createTextField(EmpDept.DEPARTMENT_NAME));
    TextFields.makeUpperCase(createTextField(EmpDept.DEPARTMENT_LOCATION));

    //we don't allow editing of the department number since it's a primary key
    getEditModel().getPrimaryKeyNullObserver().addListener(() -> {
      if (getEditModel().isEntityNew()) {
        departmentIdField.setEnabled(true);
        setInitialFocusProperty(EmpDept.DEPARTMENT_ID);
      }
      else {
        departmentIdField.setEnabled(false);
        setInitialFocusProperty(EmpDept.DEPARTMENT_NAME);
      }
    });

    setLayout(new GridLayout(3, 1, 5, 5));

    addPropertyPanel(EmpDept.DEPARTMENT_ID);
    addPropertyPanel(EmpDept.DEPARTMENT_NAME);
    addPropertyPanel(EmpDept.DEPARTMENT_LOCATION);
  }
}
/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.ui;

import org.jminor.common.db.reports.ReportWrapper;
import org.jminor.common.state.StateObserver;
import org.jminor.framework.demos.empdept.domain.EmpDept;
import org.jminor.framework.domain.entity.Entities;
import org.jminor.plugin.jasperreports.model.JasperReportsWrapper;
import org.jminor.plugin.jasperreports.ui.JasperReportsUIWrapper;
import org.jminor.swing.common.ui.control.ControlSet;
import org.jminor.swing.common.ui.control.Controls;
import org.jminor.swing.framework.model.SwingEntityTableModel;
import org.jminor.swing.framework.ui.EntityTablePanel;
import org.jminor.swing.framework.ui.reporting.EntityReportUiUtil;

import java.util.Collection;
import java.util.HashMap;

public class DepartmentTablePanel extends EntityTablePanel {

  public DepartmentTablePanel(final SwingEntityTableModel tableModel) {
    super(tableModel);
  }

  public void viewEmployeeReport() throws Exception {
    final String reportPath = ReportWrapper.getReportPath() + "/empdept_employees.jasper";
    final Collection<Integer> departmentNumbers =
            Entities.getDistinctValues(EmpDept.DEPARTMENT_ID,
                    getTableModel().getSelectionModel().getSelectedItems());
    final HashMap<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("DEPTNO", departmentNumbers);
    EntityReportUiUtil.viewJdbcReport(DepartmentTablePanel.this,
            new JasperReportsWrapper(reportPath, reportParameters),
            new JasperReportsUIWrapper(), "Employee Report", getTableModel().getConnectionProvider());
  }

  @Override
  protected ControlSet getPrintControls() {
    final ControlSet printControlSet = super.getPrintControls();
    final StateObserver selectionNotEmptyObserver =
            getTableModel().getSelectionModel().getSelectionNotEmptyObserver();
    printControlSet.add(Controls.control(this::viewEmployeeReport,
            "Employee Report", selectionNotEmptyObserver));

    return printControlSet;
  }
}
/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.ui;

import org.jminor.framework.demos.empdept.domain.EmpDept;
import org.jminor.swing.common.ui.layout.FlexibleGridLayout;
import org.jminor.swing.common.ui.textfield.TextFields;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class EmployeeEditPanel extends EntityEditPanel {

  public EmployeeEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(EmpDept.EMPLOYEE_NAME);

    final JTextField nameField = TextFields.makeUpperCase(createTextField(EmpDept.EMPLOYEE_NAME));
    nameField.setColumns(8);
    createValueListComboBox(EmpDept.EMPLOYEE_JOB);
    final JComboBox managerBox = createForeignKeyComboBox(EmpDept.EMPLOYEE_MGR_FK);
    managerBox.setPreferredSize(TextFields.getPreferredTextFieldSize());
    createForeignKeyComboBox(EmpDept.EMPLOYEE_DEPARTMENT_FK);
    createTextField(EmpDept.EMPLOYEE_SALARY);
    createTextField(EmpDept.EMPLOYEE_COMMISSION);
    createTemporalInputPanel(EmpDept.EMPLOYEE_HIREDATE, true);

    setLayout(new FlexibleGridLayout(3, 3, 5, 5, true, false));

    addPropertyPanel(EmpDept.EMPLOYEE_NAME);
    addPropertyPanel(EmpDept.EMPLOYEE_JOB);
    addPropertyPanel(EmpDept.EMPLOYEE_DEPARTMENT_FK);

    addPropertyPanel(EmpDept.EMPLOYEE_MGR_FK);
    addPropertyPanel(EmpDept.EMPLOYEE_SALARY);
    addPropertyPanel(EmpDept.EMPLOYEE_COMMISSION);

    addPropertyPanel(EmpDept.EMPLOYEE_HIREDATE);
    add(new JLabel());
    add(new JLabel());
  }
}
/*
 * Copyright (c) 2004 - 2020, Björn Darri Sigurðsson. All Rights Reserved.
 */
package org.jminor.framework.demos.empdept.ui;

import org.jminor.common.Text;
import org.jminor.common.model.CancelException;
import org.jminor.common.model.table.ColumnSummary;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.demos.empdept.domain.EmpDept;
import org.jminor.framework.demos.empdept.model.EmployeeEditModel;
import org.jminor.framework.model.EntityEditModel;
import org.jminor.plugin.json.EntityJSONParser;
import org.jminor.swing.common.ui.Windows;
import org.jminor.swing.common.ui.control.ControlSet;
import org.jminor.swing.common.ui.control.Controls;
import org.jminor.swing.common.ui.dialog.Dialogs;
import org.jminor.swing.framework.model.SwingEntityApplicationModel;
import org.jminor.swing.framework.model.SwingEntityModel;
import org.jminor.swing.framework.model.SwingEntityModelBuilder;
import org.jminor.swing.framework.model.SwingEntityTableModel;
import org.jminor.swing.framework.ui.EntityApplicationPanel;
import org.jminor.swing.framework.ui.EntityPanel;
import org.jminor.swing.framework.ui.EntityPanelBuilder;
import org.jminor.swing.framework.ui.EntityTablePanel;

import java.io.File;
import java.nio.charset.Charset;

public class EmpDeptAppPanel extends EntityApplicationPanel<EmpDeptAppPanel.EmpDeptApplicationModel> {

  @Override
  protected void setupEntityPanelBuilders() {
    final EmployeeModelBuilder employeeModelBuilder = new EmployeeModelBuilder();
    final EmployeePanelBuilder employeePanelBuilder =
            new EmployeePanelBuilder(employeeModelBuilder);
    employeePanelBuilder.setEditPanelClass(EmployeeEditPanel.class);

    final SwingEntityModelBuilder departmentModelBuilder = new SwingEntityModelBuilder(EmpDept.T_DEPARTMENT) {
      @Override
      protected void configureModel(final SwingEntityModel entityModel) {
        entityModel.getDetailModel(EmpDept.T_EMPLOYEE).getTableModel().getQueryConditionRequiredState().set(false);
      }
    };
    //This relies on the foreign key association between employee and department
    departmentModelBuilder.addDetailModelBuilder(employeeModelBuilder);

    final EntityPanelBuilder departmentPanelBuilder =
            new EntityPanelBuilder(departmentModelBuilder);
    departmentPanelBuilder.setEditPanelClass(DepartmentEditPanel.class);
    departmentPanelBuilder.setTablePanelClass(DepartmentTablePanel.class);
    departmentPanelBuilder.addDetailPanelBuilder(employeePanelBuilder);

    addEntityPanelBuilder(departmentPanelBuilder);
  }

  public void importJSON() throws Exception {
    final File file = Dialogs.selectFile(this, null);
    Dialogs.displayInDialog(this, EntityTablePanel.createReadOnlyEntityTablePanel(
            new EntityJSONParser(getModel().getDomain()).deserializeEntities(
                    Text.getTextFileContents(file.getAbsolutePath(), Charset.defaultCharset())), getModel().getConnectionProvider()), "Import");
  }

  @Override
  protected ControlSet getToolsControlSet() {
    final ControlSet toolsSet = super.getToolsControlSet();
    toolsSet.add(Controls.control(this::importJSON, "Import JSON"));

    return toolsSet;
  }

  @Override
  protected EmpDeptApplicationModel initializeApplicationModel(final EntityConnectionProvider connectionProvider) throws CancelException {
    return new EmpDeptApplicationModel(connectionProvider);
  }

  public static void main(final String[] args) {
    EntityEditModel.POST_EDIT_EVENTS.set(true);
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    EntityPanel.COMPACT_ENTITY_PANEL_LAYOUT.set(true);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("org.jminor.framework.demos.empdept.domain.EmpDept");
    new EmpDeptAppPanel().startApplication("Emp-Dept", null, false,
            Windows.getScreenSizeRatio(0.6), Users.parseUser("scott:tiger"));
  }

  public static final class EmpDeptApplicationModel extends SwingEntityApplicationModel {

    public EmpDeptApplicationModel(final EntityConnectionProvider connectionProvider) {
      super(connectionProvider);
    }
  }

  private static final class EmployeeModelBuilder extends SwingEntityModelBuilder {
    private EmployeeModelBuilder() {
      super(EmpDept.T_EMPLOYEE);
      setEditModelClass(EmployeeEditModel.class);
    }

    @Override
    protected void configureTableModel(final SwingEntityTableModel tableModel) {
      tableModel.getColumnSummaryModel(EmpDept.EMPLOYEE_SALARY).setSummary(ColumnSummary.AVERAGE);
    }
  }

  private static final class EmployeePanelBuilder extends EntityPanelBuilder {
    private EmployeePanelBuilder(final EmployeeModelBuilder modelProvider) {
      super(modelProvider);
    }

    @Override
    protected void configureTablePanel(final EntityTablePanel tablePanel) {
      tablePanel.setSummaryPanelVisible(true);
    }
  }
}