1. Screenshots

Departments
departments

2. Domain

We start by creating a class named EmpDept in a package of our choosing, extending Domain and define the identity and attribute constants required for the department Entity, which is based on the SCOTT.DEPT table. The EntityType constant represents the entity type, the attributes represent the columns in the table. The entityType 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 attribute names are 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 DefaultDomain {

  /** The domain type identifying this domain model*/
  static final DomainType DOMAIN = domainType(EmpDept.class);

  /** Entity type for the table scott.dept*/
  public interface Department extends Entity {
    EntityType<Department> TYPE = DOMAIN.entityType("scott.dept", Department.class);

    /** Attributes for the columns in the scott.dept table*/
    Attribute<Integer> ID = TYPE.integerAttribute("deptno");
    Attribute<String> NAME = TYPE.stringAttribute("dname");
    Attribute<String> LOCATION = TYPE.stringAttribute("loc");

    /** Bean getters and setters */
    Integer getId();
    void setId(Integer id);
    String getName();
    void setName(String name);
    String getLocation();
    void setLocation(String location);
  }

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

  /** Entity type for the table scott.emp*/
  public interface Employee extends Entity {
    EntityType<Employee> TYPE = DOMAIN.entityType("scott.emp", Employee.class);

    /** Attributes for the columns in the scott.emp table*/
    Attribute<Integer> ID = TYPE.integerAttribute("empno");
    Attribute<String> NAME = TYPE.stringAttribute("ename");
    Attribute<String> JOB = TYPE.stringAttribute("job");
    Attribute<Integer> MGR = TYPE.integerAttribute("mgr");
    Attribute<LocalDate> HIREDATE = TYPE.localDateAttribute("hiredate");
    Attribute<BigDecimal> SALARY = TYPE.bigDecimalAttribute("sal");
    Attribute<Double> COMMISSION = TYPE.doubleAttribute("comm");
    Attribute<Integer> DEPARTMENT = TYPE.integerAttribute("deptno");

    /**Foreign key (reference) attribute for the DEPT column in the table scott.emp*/
    ForeignKey DEPARTMENT_FK = TYPE.foreignKey("dept_fk", Employee.DEPARTMENT, Department.ID);
    /**Foreign key (reference) attribute for the MGR column in the table scott.emp*/
    ForeignKey MGR_FK = TYPE.foreignKey("mgr_fk", Employee.MGR, Employee.ID);
    /**Attribute for the denormalized department location property*/
    Attribute<String> DEPARTMENT_LOCATION = TYPE.stringAttribute("location");

    JRReportType EMPLOYEE_REPORT = JasperReports.reportType("employee_report");

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

    /** Bean getters and setters */
    Integer getId();
    void setId(Integer id);
    String getName();
    void setName(String name);
    String getJob();
    void setJob(String job);
    Employee getManager();
    void setManager(Employee manager);
    LocalDate getHiredate();
    void setHiredate(LocalDate hiredate);
    BigDecimal getSalary();
    void setSalary(BigDecimal salary);
    Double getCommission();
    void setCommission( Double commission);
    Department getDepartment();
    void setDepartment(Department department);
  }

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

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

Next we add the method defining the department entity. EntityDefinition.Builder instances are provided by the Domain class, via the define method which comes in two flavours, one which assumes the entityType 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 EntityDefinition.Builder class provides chained setters for configuring the entity, such as the primary key value source. Here we set the default order by, the so-called stringFactory 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 Department.TYPE*/
    define(Department.TYPE,
            primaryKeyProperty(Department.ID, "Department no.")
                    .updatable(true)
                    .nullable(false)
                    .beanProperty("id"),
            columnProperty(Department.NAME, "Department name")
                    .preferredColumnWidth(120)
                    .maximumLength(14)
                    .nullable(false)
                    .beanProperty("name"),
            columnProperty(Department.LOCATION, "Location")
                    .preferredColumnWidth(150)
                    .maximumLength(13)
                    .beanProperty("location"))
            .smallDataset()
            .orderBy(orderBy().ascending(Department.NAME))
            .stringFactory(stringFactory(Department.NAME))
            .caption("Departments");
  }

Next we define the employee entity. Here we set the keyGenerator to KeyGenerator.increment("scott.emp", Employee.ID.getName()) 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 Employee.TYPE*/
    define(Employee.TYPE,
            primaryKeyProperty(Employee.ID, "Employee no.")
                    .beanProperty("id"),
            columnProperty(Employee.NAME, "Name")
                    .searchProperty()
                    .maximumLength(10)
                    .nullable(false)
                    .beanProperty("name"),
            columnProperty(Employee.DEPARTMENT)
                    .nullable(false),
            foreignKeyProperty(Employee.DEPARTMENT_FK, "Department")
                    .beanProperty("department"),
            itemProperty(Employee.JOB, "Job", Employee.JOB_VALUES)
                    .beanProperty("job"),
            columnProperty(Employee.SALARY, "Salary")
                    .nullable(false)
                    .minimumValue(1000)
                    .maximumValue(10000)
                    .maximumFractionDigits(2)
                    .beanProperty("salary"),
            columnProperty(Employee.COMMISSION, "Commission")
                    .minimumValue(100)
                    .maximumValue(2000)
                    .maximumFractionDigits(2)
                    .beanProperty("commission"),
            columnProperty(Employee.MGR),
            foreignKeyProperty(Employee.MGR_FK, "Manager")
                    .beanProperty("manager"),
            columnProperty(Employee.HIREDATE, "Hiredate")
                    .nullable(false)
                    .beanProperty("hiredate")
                    .localeDateTimePattern(LocaleDateTimePattern.builder()
                            .delimiterDash()
                            .yearFourDigits()
                            .build()),
            denormalizedViewProperty(Employee.DEPARTMENT_LOCATION, "Location",
                    Employee.DEPARTMENT_FK, Department.LOCATION)
                    .preferredColumnWidth(100))
            .keyGenerator(increment("scott.emp", Employee.ID.getName()))
            .orderBy(orderBy().ascending(Employee.DEPARTMENT, Employee.NAME))
            .stringFactory(stringFactory(Employee.NAME))
            .caption("Employee")
            .colorProvider((entity, attribute) -> {
              if (attribute.equals(Employee.JOB) && "MANAGER".equals(entity.get(Employee.JOB))) {
                return Color.CYAN;
              }

              return null;
            });

    defineReport(Employee.EMPLOYEE_REPORT, classPathReport(EmpDept.class, "empdept_employees.jasper"));
  }
}

2.1. Domain unit test

To unit test the domain model we extend EntityTestUnit and create a public test method for each entity we want tested and within that method call test(entityType). 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(entityType, 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.

Note
The EntityTestUnit.TEST_USER configuration value specifies the user credentials to use when running the tests, you can set it directly or via the codion.test.user system property.
public class EmpDeptTest extends EntityTestUnit {

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

  @Test
  void department() throws Exception {
    test(Department.TYPE);
  }

  @Test
  void employee() throws Exception {
    test(Employee.TYPE);
  }
}

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

-Dcodion.db.url=jdbc:h2:mem:h2db -Dcodion.test.user=scott:tiger

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

3. 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(Employee.TYPE, 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 attribute (EmpDept.EMPLOYEE_MGR_FK) is used when referring to the manager property, very rarely do we have to worry about the underlying reference attribute (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 attribute, which only shows managers and the president */
  @Override
  public SwingEntityComboBoxModel createForeignKeyComboBoxModel(final ForeignKey foreignKey) {
    final SwingEntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKey);
    if (foreignKey.equals(Employee.MGR_FK)) {
      //Customize the null value so that it displays the chosen
      //text instead of the default '-' character
      comboBoxModel.setNullString("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.setSelectConditionSupplier(() ->
              Conditions.where(Employee.JOB).equalTo("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
    addEntitiesEditedListener(() -> getForeignKeyComboBoxModel(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(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(Employee.MGR_FK).setIncludeCondition(manager ->
              Objects.equals(manager.getForeignKey(Employee.DEPARTMENT_FK), valueChange.getValue())
                      && !Objects.equals(manager, getEntity()));
    });
  }
}

4. 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(attribute) or createForeignKeyComboBox(foreignKey).

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Department.ID);

    final JTextField departmentIdField = createTextField(Department.ID)
            .build();
    createTextField(Department.NAME)
            .upperCase(true);
    createTextField(Department.LOCATION)
            .upperCase(true);

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

    setLayout(gridLayout(3, 1));

    addInputPanel(Department.ID);
    addInputPanel(Department.NAME);
    addInputPanel(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 Collection<Integer> departmentNumbers =
            Entity.getDistinct(Department.ID,
                    getTableModel().getSelectionModel().getSelectedItems());
    final HashMap<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("DEPTNO", departmentNumbers);
    EntityReports.viewJdbcReport(DepartmentTablePanel.this, Employee.EMPLOYEE_REPORT,
            reportParameters, JRViewer::new, "Employee Report", getTableModel().getConnectionProvider());
  }

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

  @Override
  protected Controls createPrintControls() {
    final StateObserver selectionNotEmptyObserver =
            getTableModel().getSelectionModel().getSelectionNotEmptyObserver();

    return super.createPrintControls()
            .add(Control.builder(this::viewEmployeeReport)
                    .caption("Employee Report")
                    .enabledState(selectionNotEmptyObserver)
                    .build());
  }
}

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() {
    setInitialFocusAttribute(Employee.NAME);

    createTextField(Employee.NAME)
            .columns(8)
            .upperCase(true);
    createItemComboBox(Employee.JOB);
    createForeignKeyComboBox(Employee.MGR_FK);
    createTextField(Employee.SALARY);
    createTextField(Employee.COMMISSION);
    createTemporalInputPanel(Employee.HIREDATE);

    setLayout(flexibleGridLayoutBuilder()
            .rowsColumns(3, 3)
            .fixRowHeights(true)
            .build());

    addInputPanel(Employee.NAME);
    addInputPanel(Employee.JOB);
    addInputPanel(Employee.DEPARTMENT_FK, initializeDepartmentPanel());

    addInputPanel(Employee.MGR_FK);
    addInputPanel(Employee.SALARY);
    addInputPanel(Employee.COMMISSION);

    addInputPanel(Employee.HIREDATE);
    add(new JLabel());
    add(new JLabel());
  }

  private JPanel initializeDepartmentPanel() {
    final EntityComboBox departmentBox = createForeignKeyComboBox(Employee.DEPARTMENT_FK).build();
    final IntegerField departmentIdField = departmentBox.integerFieldSelector(Department.ID);
    transferFocusOnEnter(departmentIdField);

    final JPanel departmentPanel = new JPanel(borderLayout());
    departmentPanel.add(departmentIdField, BorderLayout.WEST);
    departmentPanel.add(departmentBox, BorderLayout.CENTER);

    setComponent(Employee.DEPARTMENT_FK, departmentIdField);

    return departmentPanel;
  }
}

4.1. Main application panel

We create a main application panel by extending EntityApplicationPanel. Overriding initializeEntityPanels() we create two EntityPanels using the SwingEntityModels from the application model and return the department panel, which will act as the root panel.

public class EmpDeptAppPanel extends EntityApplicationPanel<EmpDeptAppPanel.EmpDeptApplicationModel> {

  public EmpDeptAppPanel() {
    super("Emp-Dept");
  }

  @Override
  protected List<EntityPanel> initializeEntityPanels(final EmpDeptApplicationModel applicationModel) {
    final SwingEntityModel departmentModel = applicationModel.getEntityModel(Department.TYPE);
    final SwingEntityModel employeeModel = departmentModel.getDetailModel(Employee.TYPE);

    final EntityPanel employeePanelBuilder = new EntityPanel(employeeModel,
            new EmployeeEditPanel(employeeModel.getEditModel()));;

    final EntityPanel departmentPanel = new EntityPanel(departmentModel,
            new DepartmentEditPanel(departmentModel.getEditModel()),
            new DepartmentTablePanel(departmentModel.getTableModel()));
    departmentPanel.addDetailPanel(employeePanelBuilder);

    departmentModel.refresh();

    return Collections.singletonList(departmentPanel);
  }

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.fileSelectionDialogBuilder()
            .owner(this)
            .selectFile();

    final EntityTablePanel tablePanel = EntityTablePanel.createReadOnlyEntityTablePanel(
            new EntityObjectMapper(getModel().getEntities()).deserializeEntities(
                    Text.getTextFileContents(file.getAbsolutePath(), Charset.defaultCharset())), getModel().getConnectionProvider());

    Dialogs.componentDialogBuilder(tablePanel)
            .owner(this)
            .title("Import")
            .show();
  }

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

  @Override
  protected Controls getToolsControls() {
    return super.getToolsControls()
            .add(Control.builder(this::importJSON)
                    .caption("Import JSON")
                    .build());
  }

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);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.empdept.domain.EmpDept");
    SwingUtilities.invokeLater(() -> new EmpDeptAppPanel().starter()
            .frameSize(Windows.getScreenSizeRatio(0.6))
            .defaultLoginUser(User.parseUser("scott:tiger"))
            .start());
  }

We then define the EmpDeptApplicationModel class by extending SwingEntityApplicationModel.

  public static final class EmpDeptApplicationModel extends SwingEntityApplicationModel {

    public EmpDeptApplicationModel(final EntityConnectionProvider connectionProvider) {
      super(connectionProvider);
      final SwingEntityModel departmentModel = new SwingEntityModel(Department.TYPE, connectionProvider);
      departmentModel.addDetailModel(new SwingEntityModel(new EmployeeEditModel(connectionProvider)));
      addEntityModel(departmentModel);
    }
  }

5. Load test

public final class EmpDeptLoadTest extends EntityLoadTestModel<EmpDeptAppPanel.EmpDeptApplicationModel> {

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

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

  @Override
  protected EmpDeptAppPanel.EmpDeptApplicationModel initializeApplication() throws CancelException {
    final EmpDeptAppPanel.EmpDeptApplicationModel applicationModel = new EmpDeptAppPanel.EmpDeptApplicationModel(
            EntityConnectionProvider.connectionProvider().setDomainClassName(EmpDept.class.getName())
                    .setClientTypeId(EmpDeptLoadTest.class.getSimpleName())
                    .setUser(getUser()));

    final SwingEntityModel model = applicationModel.getEntityModel(Department.TYPE);
    model.addLinkedDetailModel(model.getDetailModel(Employee.TYPE));
    try {
      model.refresh();
    }
    catch (final Exception ignored) {/*ignored*/}

    return applicationModel;
  }

  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 {
        new LoadTestPanel<>(new EmpDeptLoadTest()).showFrame();
      }
      catch (final Exception e) {
        e.printStackTrace();
      }
    }
  }
}
public final class InsertDepartment extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {

  @Override
  protected void perform(final EmpDeptAppPanel.EmpDeptApplicationModel application) throws ScenarioException {
    try {
      final SwingEntityModel departmentModel = application.getEntityModel(Department.TYPE);
      departmentModel.getEditModel().setEntity(createRandomEntity(application.getEntities(),
              Department.TYPE, null));
      departmentModel.getEditModel().insert();
    }
    catch (final Exception e) {
      throw new ScenarioException(e);
    }
  }
}
public final class InsertEmployee extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {

  @Override
  protected void perform(final EmpDeptAppPanel.EmpDeptApplicationModel application) throws ScenarioException {
    try {
      final SwingEntityModel departmentModel = application.getEntityModel(Department.TYPE);
      selectRandomRow(departmentModel.getTableModel());
      final SwingEntityModel employeeModel = departmentModel.getDetailModel(Employee.TYPE);
      final Map<EntityType<?>, Entity> references = new HashMap<>();
      references.put(Department.TYPE, departmentModel.getTableModel().getSelectionModel().getSelectedItem());
      employeeModel.getEditModel().setEntity(createRandomEntity(application.getEntities(),
              Employee.TYPE, references));
      employeeModel.getEditModel().insert();
    }
    catch (final Exception e) {
      throw new ScenarioException(e);
    }
  }

  @Override
  public int getDefaultWeight() {
    return 3;
  }
}
public final class LoginLogout extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {

  final Random random = new Random();

  @Override
  protected void perform(final EmpDeptAppPanel.EmpDeptApplicationModel application) {
    try {
      application.getConnectionProvider().close();
      Thread.sleep(random.nextInt(1500));
      application.getConnectionProvider().getConnection();
    }
    catch (final InterruptedException ignored) {/*ignored*/}
  }

  @Override
  public int getDefaultWeight() {
    return 4;
  }
}
public final class SelectDepartment extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {

  @Override
  protected void perform(final EmpDeptAppPanel.EmpDeptApplicationModel application) {
    selectRandomRow(application.getEntityModel(Department.TYPE).getTableModel());
  }

  @Override
  public int getDefaultWeight() {
    return 10;
  }
}
public final class UpdateEmployee extends AbstractEntityUsageScenario<EmpDeptAppPanel.EmpDeptApplicationModel> {

  private final Random random = new Random();

  @Override
  protected void perform(final EmpDeptAppPanel.EmpDeptApplicationModel application) throws ScenarioException {
    try {
      final SwingEntityModel departmentModel = application.getEntityModel(Department.TYPE);
      selectRandomRow(departmentModel.getTableModel());
      final SwingEntityModel employeeModel = departmentModel.getDetailModel(Employee.TYPE);
      if (employeeModel.getTableModel().getRowCount() > 0) {
        final EntityConnection connection = employeeModel.getConnectionProvider().getConnection();
        connection.beginTransaction();
        try {
          selectRandomRow(employeeModel.getTableModel());
          Entity selected = employeeModel.getTableModel().getSelectionModel().getSelectedItem();
          randomize(application.getEntities(), selected, null);
          employeeModel.getEditModel().setEntity(selected);
          employeeModel.getEditModel().update();
          selectRandomRow(employeeModel.getTableModel());
          selected = employeeModel.getTableModel().getSelectionModel().getSelectedItem();
          randomize(application.getEntities(), selected, null);
          employeeModel.getEditModel().setEntity(selected);
          employeeModel.getEditModel().update();
        }
        finally {
          if (random.nextBoolean()) {
            connection.rollbackTransaction();
          }
          else {
            connection.commitTransaction();
          }
        }
      }
    }
    catch (final Exception e) {
      throw new ScenarioException(e);
    }
  }

  @Override
  public int getDefaultWeight() {
    return 5;
  }
}

6. Full Demo Source

6.1. Domain

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

import is.codion.common.formats.LocaleDateTimePattern;
import is.codion.common.item.Item;
import is.codion.framework.domain.DefaultDomain;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.Attribute;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.ForeignKey;
import is.codion.plugin.jasperreports.model.JRReportType;
import is.codion.plugin.jasperreports.model.JasperReports;

import java.awt.Color;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

import static is.codion.common.item.Item.item;
import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.KeyGenerator.increment;
import static is.codion.framework.domain.entity.OrderBy.orderBy;
import static is.codion.framework.domain.entity.StringFactory.stringFactory;
import static is.codion.framework.domain.property.Properties.*;
import static is.codion.plugin.jasperreports.model.JasperReports.classPathReport;
import static java.util.Arrays.asList;

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

  /** The domain type identifying this domain model*/
  static final DomainType DOMAIN = domainType(EmpDept.class);

  /** Entity type for the table scott.dept*/
  public interface Department extends Entity {
    EntityType<Department> TYPE = DOMAIN.entityType("scott.dept", Department.class);

    /** Attributes for the columns in the scott.dept table*/
    Attribute<Integer> ID = TYPE.integerAttribute("deptno");
    Attribute<String> NAME = TYPE.stringAttribute("dname");
    Attribute<String> LOCATION = TYPE.stringAttribute("loc");

    /** Bean getters and setters */
    Integer getId();
    void setId(Integer id);
    String getName();
    void setName(String name);
    String getLocation();
    void setLocation(String location);
  }

  /** Entity type for the table scott.emp*/
  public interface Employee extends Entity {
    EntityType<Employee> TYPE = DOMAIN.entityType("scott.emp", Employee.class);

    /** Attributes for the columns in the scott.emp table*/
    Attribute<Integer> ID = TYPE.integerAttribute("empno");
    Attribute<String> NAME = TYPE.stringAttribute("ename");
    Attribute<String> JOB = TYPE.stringAttribute("job");
    Attribute<Integer> MGR = TYPE.integerAttribute("mgr");
    Attribute<LocalDate> HIREDATE = TYPE.localDateAttribute("hiredate");
    Attribute<BigDecimal> SALARY = TYPE.bigDecimalAttribute("sal");
    Attribute<Double> COMMISSION = TYPE.doubleAttribute("comm");
    Attribute<Integer> DEPARTMENT = TYPE.integerAttribute("deptno");

    /**Foreign key (reference) attribute for the DEPT column in the table scott.emp*/
    ForeignKey DEPARTMENT_FK = TYPE.foreignKey("dept_fk", Employee.DEPARTMENT, Department.ID);
    /**Foreign key (reference) attribute for the MGR column in the table scott.emp*/
    ForeignKey MGR_FK = TYPE.foreignKey("mgr_fk", Employee.MGR, Employee.ID);
    /**Attribute for the denormalized department location property*/
    Attribute<String> DEPARTMENT_LOCATION = TYPE.stringAttribute("location");

    JRReportType EMPLOYEE_REPORT = JasperReports.reportType("employee_report");

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

    /** Bean getters and setters */
    Integer getId();
    void setId(Integer id);
    String getName();
    void setName(String name);
    String getJob();
    void setJob(String job);
    Employee getManager();
    void setManager(Employee manager);
    LocalDate getHiredate();
    void setHiredate(LocalDate hiredate);
    BigDecimal getSalary();
    void setSalary(BigDecimal salary);
    Double getCommission();
    void setCommission( Double commission);
    Department getDepartment();
    void setDepartment(Department department);
  }

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

  void department() {
    /*Defining the entity Department.TYPE*/
    define(Department.TYPE,
            primaryKeyProperty(Department.ID, "Department no.")
                    .updatable(true)
                    .nullable(false)
                    .beanProperty("id"),
            columnProperty(Department.NAME, "Department name")
                    .preferredColumnWidth(120)
                    .maximumLength(14)
                    .nullable(false)
                    .beanProperty("name"),
            columnProperty(Department.LOCATION, "Location")
                    .preferredColumnWidth(150)
                    .maximumLength(13)
                    .beanProperty("location"))
            .smallDataset()
            .orderBy(orderBy().ascending(Department.NAME))
            .stringFactory(stringFactory(Department.NAME))
            .caption("Departments");
  }

  void employee() {
    /*Defining the entity Employee.TYPE*/
    define(Employee.TYPE,
            primaryKeyProperty(Employee.ID, "Employee no.")
                    .beanProperty("id"),
            columnProperty(Employee.NAME, "Name")
                    .searchProperty()
                    .maximumLength(10)
                    .nullable(false)
                    .beanProperty("name"),
            columnProperty(Employee.DEPARTMENT)
                    .nullable(false),
            foreignKeyProperty(Employee.DEPARTMENT_FK, "Department")
                    .beanProperty("department"),
            itemProperty(Employee.JOB, "Job", Employee.JOB_VALUES)
                    .beanProperty("job"),
            columnProperty(Employee.SALARY, "Salary")
                    .nullable(false)
                    .minimumValue(1000)
                    .maximumValue(10000)
                    .maximumFractionDigits(2)
                    .beanProperty("salary"),
            columnProperty(Employee.COMMISSION, "Commission")
                    .minimumValue(100)
                    .maximumValue(2000)
                    .maximumFractionDigits(2)
                    .beanProperty("commission"),
            columnProperty(Employee.MGR),
            foreignKeyProperty(Employee.MGR_FK, "Manager")
                    .beanProperty("manager"),
            columnProperty(Employee.HIREDATE, "Hiredate")
                    .nullable(false)
                    .beanProperty("hiredate")
                    .localeDateTimePattern(LocaleDateTimePattern.builder()
                            .delimiterDash()
                            .yearFourDigits()
                            .build()),
            denormalizedViewProperty(Employee.DEPARTMENT_LOCATION, "Location",
                    Employee.DEPARTMENT_FK, Department.LOCATION)
                    .preferredColumnWidth(100))
            .keyGenerator(increment("scott.emp", Employee.ID.getName()))
            .orderBy(orderBy().ascending(Employee.DEPARTMENT, Employee.NAME))
            .stringFactory(stringFactory(Employee.NAME))
            .caption("Employee")
            .colorProvider((entity, attribute) -> {
              if (attribute.equals(Employee.JOB) && "MANAGER".equals(entity.get(Employee.JOB))) {
                return Color.CYAN;
              }

              return null;
            });

    defineReport(Employee.EMPLOYEE_REPORT, classPathReport(EmpDept.class, "empdept_employees.jasper"));
  }
}

6.2. Domain unit test

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

import is.codion.framework.demos.empdept.domain.EmpDept.Department;
import is.codion.framework.demos.empdept.domain.EmpDept.Employee;
import is.codion.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

public class EmpDeptTest extends EntityTestUnit {

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

  @Test
  void department() throws Exception {
    test(Department.TYPE);
  }

  @Test
  void employee() throws Exception {
    test(Employee.TYPE);
  }
}

6.3. Model

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

import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.condition.Conditions;
import is.codion.framework.demos.empdept.domain.EmpDept.Employee;
import is.codion.framework.domain.entity.ForeignKey;
import is.codion.swing.framework.model.SwingEntityComboBoxModel;
import is.codion.swing.framework.model.SwingEntityEditModel;

import java.util.Objects;

public final class EmployeeEditModel extends SwingEntityEditModel {

  public EmployeeEditModel(final EntityConnectionProvider connectionProvider) {
    super(Employee.TYPE, connectionProvider);
    bindEvents();
  }

  /** Providing a custom ComboBoxModel for the manager attribute, which only shows managers and the president */
  @Override
  public SwingEntityComboBoxModel createForeignKeyComboBoxModel(final ForeignKey foreignKey) {
    final SwingEntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKey);
    if (foreignKey.equals(Employee.MGR_FK)) {
      //Customize the null value so that it displays the chosen
      //text instead of the default '-' character
      comboBoxModel.setNullString("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.setSelectConditionSupplier(() ->
              Conditions.where(Employee.JOB).equalTo("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
    addEntitiesEditedListener(() -> getForeignKeyComboBoxModel(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(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(Employee.MGR_FK).setIncludeCondition(manager ->
              Objects.equals(manager.getForeignKey(Employee.DEPARTMENT_FK), valueChange.getValue())
                      && !Objects.equals(manager, getEntity()));
    });
  }
}

6.4. UI

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

import is.codion.framework.demos.empdept.domain.EmpDept.Department;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import javax.swing.JTextField;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public class DepartmentEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Department.ID);

    final JTextField departmentIdField = createTextField(Department.ID)
            .build();
    createTextField(Department.NAME)
            .upperCase(true);
    createTextField(Department.LOCATION)
            .upperCase(true);

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

    setLayout(gridLayout(3, 1));

    addInputPanel(Department.ID);
    addInputPanel(Department.NAME);
    addInputPanel(Department.LOCATION);
  }
}
/*
 * Copyright (c) 2004 - 2021, Björn Darri Sigurðsson. All Rights Reserved.
 */
package is.codion.framework.demos.empdept.ui;

import is.codion.common.state.StateObserver;
import is.codion.framework.demos.empdept.domain.EmpDept.Department;
import is.codion.framework.demos.empdept.domain.EmpDept.Employee;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityReports;
import is.codion.swing.framework.ui.EntityTablePanel;

import net.sf.jasperreports.swing.JRViewer;

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 Collection<Integer> departmentNumbers =
            Entity.getDistinct(Department.ID,
                    getTableModel().getSelectionModel().getSelectedItems());
    final HashMap<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("DEPTNO", departmentNumbers);
    EntityReports.viewJdbcReport(DepartmentTablePanel.this, Employee.EMPLOYEE_REPORT,
            reportParameters, JRViewer::new, "Employee Report", getTableModel().getConnectionProvider());
  }

  @Override
  protected Controls createPrintControls() {
    final StateObserver selectionNotEmptyObserver =
            getTableModel().getSelectionModel().getSelectionNotEmptyObserver();

    return super.createPrintControls()
            .add(Control.builder(this::viewEmployeeReport)
                    .caption("Employee Report")
                    .enabledState(selectionNotEmptyObserver)
                    .build());
  }
}
/*
 * Copyright (c) 2004 - 2021, Björn Darri Sigurðsson. All Rights Reserved.
 */
package is.codion.framework.demos.empdept.ui;

import is.codion.framework.demos.empdept.domain.EmpDept.Department;
import is.codion.framework.demos.empdept.domain.EmpDept.Employee;
import is.codion.swing.common.ui.textfield.IntegerField;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityComboBox;
import is.codion.swing.framework.ui.EntityEditPanel;

import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.BorderLayout;

import static is.codion.swing.common.ui.Components.transferFocusOnEnter;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static is.codion.swing.common.ui.layout.Layouts.flexibleGridLayoutBuilder;

public class EmployeeEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Employee.NAME);

    createTextField(Employee.NAME)
            .columns(8)
            .upperCase(true);
    createItemComboBox(Employee.JOB);
    createForeignKeyComboBox(Employee.MGR_FK);
    createTextField(Employee.SALARY);
    createTextField(Employee.COMMISSION);
    createTemporalInputPanel(Employee.HIREDATE);

    setLayout(flexibleGridLayoutBuilder()
            .rowsColumns(3, 3)
            .fixRowHeights(true)
            .build());

    addInputPanel(Employee.NAME);
    addInputPanel(Employee.JOB);
    addInputPanel(Employee.DEPARTMENT_FK, initializeDepartmentPanel());

    addInputPanel(Employee.MGR_FK);
    addInputPanel(Employee.SALARY);
    addInputPanel(Employee.COMMISSION);

    addInputPanel(Employee.HIREDATE);
    add(new JLabel());
    add(new JLabel());
  }

  private JPanel initializeDepartmentPanel() {
    final EntityComboBox departmentBox = createForeignKeyComboBox(Employee.DEPARTMENT_FK).build();
    final IntegerField departmentIdField = departmentBox.integerFieldSelector(Department.ID);
    transferFocusOnEnter(departmentIdField);

    final JPanel departmentPanel = new JPanel(borderLayout());
    departmentPanel.add(departmentIdField, BorderLayout.WEST);
    departmentPanel.add(departmentBox, BorderLayout.CENTER);

    setComponent(Employee.DEPARTMENT_FK, departmentIdField);

    return departmentPanel;
  }
}
/*
 * Copyright (c) 2004 - 2021, Björn Darri Sigurðsson. All Rights Reserved.
 */
package is.codion.framework.demos.empdept.ui;

import is.codion.common.Text;
import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.empdept.domain.EmpDept.Department;
import is.codion.framework.demos.empdept.domain.EmpDept.Employee;
import is.codion.framework.demos.empdept.model.EmployeeEditModel;
import is.codion.framework.model.EntityEditModel;
import is.codion.plugin.jackson.json.domain.EntityObjectMapper;
import is.codion.swing.common.ui.Windows;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityApplicationModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.EntityTablePanel;

import javax.swing.SwingUtilities;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;

public class EmpDeptAppPanel extends EntityApplicationPanel<EmpDeptAppPanel.EmpDeptApplicationModel> {

  public EmpDeptAppPanel() {
    super("Emp-Dept");
  }

  @Override
  protected List<EntityPanel> initializeEntityPanels(final EmpDeptApplicationModel applicationModel) {
    final SwingEntityModel departmentModel = applicationModel.getEntityModel(Department.TYPE);
    final SwingEntityModel employeeModel = departmentModel.getDetailModel(Employee.TYPE);

    final EntityPanel employeePanelBuilder = new EntityPanel(employeeModel,
            new EmployeeEditPanel(employeeModel.getEditModel()));;

    final EntityPanel departmentPanel = new EntityPanel(departmentModel,
            new DepartmentEditPanel(departmentModel.getEditModel()),
            new DepartmentTablePanel(departmentModel.getTableModel()));
    departmentPanel.addDetailPanel(employeePanelBuilder);

    departmentModel.refresh();

    return Collections.singletonList(departmentPanel);
  }

  public void importJSON() throws Exception {
    final File file = Dialogs.fileSelectionDialogBuilder()
            .owner(this)
            .selectFile();

    final EntityTablePanel tablePanel = EntityTablePanel.createReadOnlyEntityTablePanel(
            new EntityObjectMapper(getModel().getEntities()).deserializeEntities(
                    Text.getTextFileContents(file.getAbsolutePath(), Charset.defaultCharset())), getModel().getConnectionProvider());

    Dialogs.componentDialogBuilder(tablePanel)
            .owner(this)
            .title("Import")
            .show();
  }

  @Override
  protected Controls getToolsControls() {
    return super.getToolsControls()
            .add(Control.builder(this::importJSON)
                    .caption("Import JSON")
                    .build());
  }

  @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);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.empdept.domain.EmpDept");
    SwingUtilities.invokeLater(() -> new EmpDeptAppPanel().starter()
            .frameSize(Windows.getScreenSizeRatio(0.6))
            .defaultLoginUser(User.parseUser("scott:tiger"))
            .start());
  }

  public static final class EmpDeptApplicationModel extends SwingEntityApplicationModel {

    public EmpDeptApplicationModel(final EntityConnectionProvider connectionProvider) {
      super(connectionProvider);
      final SwingEntityModel departmentModel = new SwingEntityModel(Department.TYPE, connectionProvider);
      departmentModel.addDetailModel(new SwingEntityModel(new EmployeeEditModel(connectionProvider)));
      addEntityModel(departmentModel);
    }
  }
}