2. Database
Create Schema SQL
create user if not exists scott password 'tiger';
alter user scott admin true;
create schema employees;
CREATE TABLE employees.department (
department_no INT NOT NULL,
name VARCHAR(14) NOT NULL,
location VARCHAR(13),
constraint department_pk primary key (department_no)
);
CREATE TABLE employees.employee (
id INT NOT NULL,
name VARCHAR(10) NOT NULL,
job VARCHAR(9),
manager_id INT,
hiredate DATE,
salary DECIMAL(7, 2) NOT NULL,
commission DECIMAL(7, 2),
department_no INT NOT NULL,
constraint employee_pk primary key (id),
constraint employee_department_fk foreign key (department_no) references employees.department(department_no),
constraint employee_manager_fk foreign key (manager_id) references employees.employee(id)
);
CREATE SEQUENCE employees.employee_seq START WITH 17;
INSERT INTO employees.department(department_no, name, location)
VALUES (10, 'Accounting', 'New York'),
(20, 'Research', 'Dallas'),
(30, 'Sales', 'Chicaco'),
(40, 'Operations', 'Boston');
INSERT INTO employees.employee(id, name, job, manager_id, hiredate, salary, commission, department_no)
VALUES (8, 'King', 'President', NULL, '1981-11-17', 5000, NULL, 10),
(3, 'Jones', 'Manager', 8, '1981-04-02', 2975, NULL, 20),
(5, 'Blake', 'Manager', 3, '1981-05-01', 2850, NULL, 10),
(1, 'Allen', 'Salesman', 5, '1981-02-20', 1600, 300, 30),
(2, 'Ward', 'Salesman', 8, '1981-02-22', 1250, 500, 30),
(4, 'Martin', 'Salesman', 3, '1981-09-28', 1250, 1400, 30),
(6, 'Clark', 'Manager', 8, '1981-06-09', 2450, 1500, 10),
(7, 'Scott', 'Analyst', 3, '1987-04-19', 3000, 1500, 20),
(9, 'Turner', 'Salesman', 5, '1981-09-08', 1500, 0, 30),
(10, 'Adams', 'Clerk', 3, '1988-05-15', 1100, NULL, 20),
(11, 'James', 'Clerk', 5, '1996-10-03', 950, 1500, 10),
(12, 'Ford', 'Clerk', 3, '1988-12-12', 3200, 1200, 20),
(13, 'Miller', 'Clerk', 6, '1983-01-23', 1300, 1200, 10),
(14, 'Ben', 'Clerk', 3, '1989-12-12', 1600, 1200, 20),
(15, 'Baker', 'Clerk', 5, '2007-01-01', 1000, NULL, 10),
(16, 'Trevor', 'Clerk', 5, '2007-01-01', 1000, NULL, 10);
commit;
3. Domain model
We start by creating a class named Employees in a package of our choosing, extending DefaultDomain and define the identity and column constants required for the department Entity, which is based on the EMPLOYEES.DEPARTMENT table. The EntityType constant represents the entity type, the columns represent the columns in the table. The entityType in this example contains the actual table name (employees.department), but the table name can be specified via the tableName() builder method when the entity is defined.The column names are the actual column names from the table, but these can also be specified via columnName() builder method.
// This class contains the specification for the Employees application domain model
public final class Employees extends DefaultDomain {
// The domain type identifying this domain model
public static final DomainType DOMAIN = domainType(Employees.class);
// Entity type for the table employees.department
public interface Department {
EntityType TYPE = DOMAIN.entityType("employees.department");
// Columns for the columns in the employees.department table
Column<Integer> DEPARTMENT_NO = TYPE.integerColumn("department_no");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> LOCATION = TYPE.stringColumn("location");
}
Next we define the constants required for the employee entity, which is based on the EMPLOYEES.EMPLOYEE table.Here there are two additional attributes with _FK suffixes, we’ll use these later when we specify the foreign key attributes for the employee entity.
// Entity type for the table employees.employee
public interface Employee {
EntityType TYPE = DOMAIN.entityType("employees.employee");
// Columns for the columns in the employees.employee table
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> JOB = TYPE.stringColumn("job");
Column<Integer> MANAGER_ID = TYPE.integerColumn("manager_id");
Column<LocalDate> HIREDATE = TYPE.localDateColumn("hiredate");
Column<BigDecimal> SALARY = TYPE.bigDecimalColumn("salary");
Column<Double> COMMISSION = TYPE.doubleColumn("commission");
Column<Integer> DEPARTMENT = TYPE.integerColumn("department_no");
// Foreign key attribute for the DEPTNO column in the table employees.employee
ForeignKey DEPARTMENT_FK = TYPE.foreignKey("department_no_fk", DEPARTMENT, Department.DEPARTMENT_NO);
// Foreign key attribute for the MGR column in the table employees.employee
ForeignKey MANAGER_FK = TYPE.foreignKey("manager_fk", MANAGER_ID, 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"), item("Clerk"),
item("Manager"), item("President"),
item("Salesman"));
}
In this section we add a constructor, adding two entity definition builders to the domain model, along with a employee report, loaded from the classpath.
// Initializes this domain model
public Employees() {
super(DOMAIN);
add(department(), employee());
add(Employee.EMPLOYEE_REPORT, classPathReport(Employees.class, "employees.jasper"));
}
Next we add the method defining the department entity. EntityDefinition.Builder instances are provided by the EntityDefinition class, via the definition method, which takes an array of AttributeDefinition.Builder instances as parameter. For an overview of the AttributeDefinition class see Manual#Attributes. 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.
EntityDefinition.Builder department() {
// Defining the entity Department.TYPE
return Department.TYPE.define(
Department.DEPARTMENT_NO.define()
.primaryKey()
.caption("No.")
.nullable(false),
Department.NAME.define()
.column()
.caption("Name")
.maximumLength(14)
.nullable(false),
Department.LOCATION.define()
.column()
.caption("Location")
.maximumLength(13))
.smallDataset(true)
.orderBy(ascending(Department.NAME))
.stringFactory(Department.NAME)
.caption("Department");
}
Next we define the employee entity. Here we set the keyGenerator to KeyGenerator.sequence("employees.employee_seq") which, as the name suggests, fetches a value from a sequence. Here we also introduce the ForeignKey, the orderBy, as well as a ColorProvider which is responsible for providing a custom color for an entity instance.
EntityDefinition.Builder employee() {
// Defining the entity Employee.TYPE
return Employee.TYPE.define(
Employee.ID.define()
.primaryKey(),
Employee.NAME.define()
.column()
.caption("Name")
.searchable(true)
.maximumLength(10)
.nullable(false),
Employee.DEPARTMENT.define()
.column()
.nullable(false),
Employee.DEPARTMENT_FK.define()
.foreignKey()
.caption("Department"),
Employee.JOB.define()
.column()
.caption("Job")
.items(Employee.JOB_VALUES),
Employee.SALARY.define()
.column()
.caption("Salary")
.nullable(false)
.valueRange(900, 10000)
.maximumFractionDigits(2),
Employee.COMMISSION.define()
.column()
.caption("Commission")
.valueRange(100, 2000)
.maximumFractionDigits(2),
Employee.MANAGER_ID.define()
.column(),
Employee.MANAGER_FK.define()
.foreignKey()
.caption("Manager"),
Employee.HIREDATE.define()
.column()
.caption("Hiredate")
.nullable(false)
.localeDateTimePattern(LocaleDateTimePattern.builder()
.delimiterDash()
.yearFourDigits()
.build()),
Employee.DEPARTMENT_LOCATION.define()
.denormalized(Employee.DEPARTMENT_FK, Department.LOCATION)
.caption("Location"))
.keyGenerator(sequence("employees.employee_seq"))
.orderBy(ascending(Employee.DEPARTMENT, Employee.NAME))
.stringFactory(Employee.NAME)
.caption("Employee")
.backgroundColorProvider((entity, attribute) -> {
if (attribute.equals(Employee.JOB) && "Manager".equals(entity.get(Employee.JOB))) {
return Color.CYAN;
}
return null;
})
.foregroundColorProvider((entity, attribute) -> {
if (attribute.equals(Employee.SALARY) && entity.get(Employee.SALARY).doubleValue() < 1300) {
return Color.RED;
}
return null;
});
}
}
3.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 EmployeesTest extends EntityTestUnit {
public EmployeesTest() {
super(new Employees());
}
@Test
void department() throws Exception {
test(Department.TYPE);
}
@Test
void employee() throws Exception {
test(Employee.TYPE);
}
}
Here is the parameter required for running the Employees 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.
4. 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(EntityConnectionProvider connectionProvider) {
super(Employee.TYPE, connectionProvider);
initializeComboBoxModels(Employee.MANAGER_FK, Employee.DEPARTMENT_FK);
bindEvents();
}
At some point we are going to be editing the manager attribute of an employee which means we’ll need a list of managers. We override createForeignKeyComboBoxModel in order to provide a specialized combo box model for the manager attribute.
Note
|
The foreign key (Employee.MGR_FK) is used when referring to the manager attribute, very rarely do we have to worry about the underlying reference attribute (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 that it only displays employees with the job PRESIDENT or MANAGER.
// Providing a custom ComboBoxModel for the manager attribute, which only shows managers and the president
@Override
public EntityComboBoxModel createForeignKeyComboBoxModel(ForeignKey foreignKey) {
EntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKey);
if (foreignKey.equals(Employee.MANAGER_FK)) {
//Customize the null value caption so that it displays 'None'
//instead of the default '-' character
comboBoxModel.setNullCaption("None");
//we do not want filtering to remove a value that is selected
//and thereby change the selection, see bindEvents() below
comboBoxModel.filterSelectedItem().set(false);
//Only select the president and managers from the database
comboBoxModel.condition().set(() ->
Employee.JOB.in("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
insertUpdateOrDeleteEvent().addListener(() -> foreignKeyComboBoxModel(Employee.MANAGER_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
valueEvent(Employee.DEPARTMENT_FK).addConsumer(department -> {
//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
foreignKeyComboBoxModel(Employee.MANAGER_FK).includeCondition().set(manager ->
Objects.equals(manager.entity(Employee.DEPARTMENT_FK), department)
&& !Objects.equals(manager, entity()));
});
}
}
5. 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(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() {
initialFocusAttribute().set(Department.DEPARTMENT_NO);
createTextField(Department.DEPARTMENT_NO)
.columns(3)
//don't allow editing of existing department numbers
.enabled(editModel().exists().not());
createTextField(Department.NAME)
.columns(8);
createTextField(Department.LOCATION)
.columns(12);
editModel().exists().addConsumer(exists ->
initialFocusAttribute().set(exists ? Department.NAME : Department.DEPARTMENT_NO));
setLayout(borderLayout());
add(borderLayoutPanel()
.northComponent(borderLayoutPanel()
.westComponent(createInputPanel(Department.DEPARTMENT_NO))
.centerComponent(createInputPanel(Department.NAME))
.build())
.centerComponent(createInputPanel(Department.LOCATION))
.build(), borderLayout().CENTER);
}
}
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(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.
private void viewEmployeeReport() throws ReportException, DatabaseException {
Collection<Integer> departmentNumbers =
Entity.distinct(Department.DEPARTMENT_NO,
tableModel().selectionModel().getSelectedItems());
Map<String, Object> reportParameters = new HashMap<>();
reportParameters.put("DEPTNO", departmentNumbers);
JasperPrint employeeReport = tableModel().connection()
.report(Employee.EMPLOYEE_REPORT, reportParameters);
Dialogs.componentDialog(new JRViewer(employeeReport))
.owner(this)
.modal(false)
.size(new Dimension(800, 600))
.show();
}
Next we override configureControls() to populate the print control with our custom report one.
@Override
protected void configureControls() {
control(TableControl.PRINT).set(Control.builder(this::viewEmployeeReport)
.name("Employee Report")
.enabled(tableModel().selectionModel().selectionNotEmpty())
.build());
}
For editing employee entities we create the EmployeeEditPanel class.
public class EmployeeEditPanel extends EntityEditPanel {
public EmployeeEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
All we have to do is override initializeUI.
@Override
protected void initializeUI() {
initialFocusAttribute().set(Employee.NAME);
createTextField(Employee.NAME)
.columns(8);
createItemComboBox(Employee.JOB);
createForeignKeyComboBox(Employee.MANAGER_FK);
createTextField(Employee.SALARY)
.columns(5);
createTextField(Employee.COMMISSION)
.columns(5);
createTemporalFieldPanel(Employee.HIREDATE)
.columns(6);
setLayout(flexibleGridLayout(0, 3));
addInputPanel(Employee.NAME);
addInputPanel(Employee.DEPARTMENT_FK, createDepartmentPanel());
addInputPanel(Employee.JOB);
addInputPanel(Employee.MANAGER_FK);
add(gridLayoutPanel(1, 2)
.add(createInputPanel(Employee.SALARY))
.add(createInputPanel(Employee.COMMISSION))
.build());
addInputPanel(Employee.HIREDATE);
}
private JPanel createDepartmentPanel() {
EntityComboBox departmentBox = createForeignKeyComboBox(Employee.DEPARTMENT_FK).build();
NumberField<Integer> departmentNumberField = departmentBox.integerSelectorField(Department.DEPARTMENT_NO)
.transferFocusOnEnter(true)
.onBuild(field -> addValidator(Employee.DEPARTMENT, field))
.build();
component(Employee.DEPARTMENT_FK).set(departmentNumberField);
return Components.borderLayoutPanel()
.westComponent(departmentNumberField)
.centerComponent(departmentBox)
.build();
}
}
5.1. Main application model
We create a main application model by extending SwingEntityApplicationModel and add an entity model for the department entity.
public final class EmployeesAppModel extends SwingEntityApplicationModel {
public EmployeesAppModel(EntityConnectionProvider connectionProvider) {
super(connectionProvider);
SwingEntityModel departmentModel = new SwingEntityModel(Department.TYPE, connectionProvider);
departmentModel.addDetailModel(new SwingEntityModel(new EmployeeEditModel(connectionProvider)));
departmentModel.tableModel().refresh();
addEntityModel(departmentModel);
}
}
5.2. Main application panel
We create a main application panel by extending EntityApplicationPanel. A constructor with a single EmployeesAppModel argument is required. Overriding createEntityPanels() 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 EmployeesAppPanel extends EntityApplicationPanel<EmployeesAppModel> {
private static final String DEFAULT_FLAT_LOOK_AND_FEEL = "com.formdev.flatlaf.intellijthemes.FlatArcIJTheme";
public EmployeesAppPanel(EmployeesAppModel applicationModel) {
super(applicationModel);
}
@Override
protected List<EntityPanel> createEntityPanels() {
SwingEntityModel departmentModel = applicationModel().entityModel(Department.TYPE);
SwingEntityModel employeeModel = departmentModel.detailModel(Employee.TYPE);
EntityPanel employeePanel = new EntityPanel(employeeModel,
new EmployeeEditPanel(employeeModel.editModel()));
EntityPanel departmentPanel = new EntityPanel(departmentModel,
new DepartmentEditPanel(departmentModel.editModel()),
new DepartmentTablePanel(departmentModel.tableModel()),
config -> config.detailLayout(entityPanel -> TabbedDetailLayout.builder(entityPanel)
.splitPaneResizeWeight(0.4)
.build()));
departmentPanel.addDetailPanel(employeePanel);
return 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 {
File file = Dialogs.fileSelectionDialog()
.owner(this)
.fileFilter(new FileNameExtensionFilter("JSON files", "json"))
.fileFilter(new FileNameExtensionFilter("Text files", "txt"))
.selectFile();
List<Entity> entities = entityObjectMapper(applicationModel().entities())
.deserializeEntities(String.join("\n", Files.readAllLines(file.toPath())));
SwingEntityTableModel tableModel = SwingEntityTableModel.tableModel(entities, applicationModel().connectionProvider());
tableModel.editModel().readOnly().set(true);
EntityTablePanel tablePanel = new EntityTablePanel(tableModel,
config -> config.includePopupMenu(false));
Dialogs.componentDialog(tablePanel.initialize())
.owner(this)
.title("Import")
.show();
}
We override createToolsMenuControls() to add our import action to the Tools menu.
@Override
protected Controls createToolsMenuControls() {
return super.createToolsMenuControls()
.add(Control.builder(this::importJSON)
.name("Import JSON")
.build());
}
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(String[] args) {
Arrays.stream(FlatAllIJThemes.INFOS)
.forEach(LookAndFeelProvider::addLookAndFeelProvider);
EntityPanel.Config.TOOLBAR_CONTROLS.set(true);
EntityApplicationPanel.builder(EmployeesAppModel.class, EmployeesAppPanel.class)
.applicationName("Employees")
.domainType(Employees.DOMAIN)
.defaultLookAndFeelClassName(DEFAULT_FLAT_LOOK_AND_FEEL)
.defaultLoginUser(User.parse("scott:tiger"))
.start();
}
6. Load test
public final class EmployeesLoadTest {
private static final User UNIT_TEST_USER =
User.parse(System.getProperty("codion.test.user", "scott:tiger"));
private static final class EmployeesAppModelFactory
implements Function<User, EmployeesAppModel> {
@Override
public EmployeesAppModel apply(User user) {
EmployeesAppModel applicationModel =
new EmployeesAppModel(EntityConnectionProvider.builder()
.domainType(Employees.DOMAIN)
.clientTypeId(EmployeesLoadTest.class.getSimpleName())
.user(user)
.build());
SwingEntityModel model = applicationModel.entityModel(Department.TYPE);
model.detailModelLink(model.detailModel(Employee.TYPE)).active().set(true);
try {
model.tableModel().refresh();
}
catch (Exception ignored) {/*ignored*/}
return applicationModel;
}
}
public static void main(String[] args) {
LoadTest<EmployeesAppModel> loadTest =
LoadTest.builder(new EmployeesAppModelFactory(),
application -> application.connectionProvider().close())
.user(UNIT_TEST_USER)
.scenarios(asList(
scenario(new InsertDepartment(), 1),
scenario(new InsertEmployee(), 3),
scenario(new LoginLogout(), 4),
scenario(new SelectDepartment(), 10),
scenario(new UpdateEmployee(), 5)))
.name("Employees LoadTest - " + EntityConnectionProvider.CLIENT_CONNECTION_TYPE.get())
.build();
loadTestPanel(loadTestModel(loadTest)).run();
}
}
public final class InsertDepartment implements Performer<EmployeesAppModel> {
@Override
public void perform(EmployeesAppModel application) throws Exception {
SwingEntityModel departmentModel = application.entityModel(Department.TYPE);
departmentModel.editModel().set(createRandomEntity(application.entities(), Department.TYPE, null));
departmentModel.editModel().insert();
}
}
public final class InsertEmployee implements Performer<EmployeesAppModel> {
@Override
public void perform(EmployeesAppModel application) throws Exception {
SwingEntityModel departmentModel = application.entityModel(Department.TYPE);
selectRandomRow(departmentModel.tableModel());
SwingEntityModel employeeModel = departmentModel.detailModel(Employee.TYPE);
Map<ForeignKey, Entity> foreignKeyEntities = new HashMap<>();
foreignKeyEntities.put(Employee.DEPARTMENT_FK, departmentModel.tableModel().selectionModel().getSelectedItem());
employeeModel.editModel().set(createRandomEntity(application.entities(), Employee.TYPE, foreignKeyEntities));
employeeModel.editModel().insert();
}
}
public final class LoginLogout implements Performer<EmployeesAppModel> {
final Random random = new Random();
@Override
public void perform(EmployeesAppModel application) {
try {
application.connectionProvider().close();
Thread.sleep(random.nextInt(1500));
application.connectionProvider().connection();
}
catch (InterruptedException ignored) {/*ignored*/}
}
}
public final class SelectDepartment implements Performer<EmployeesAppModel> {
@Override
public void perform(EmployeesAppModel application) {
selectRandomRow(application.entityModel(Department.TYPE).tableModel());
}
}
public final class UpdateEmployee implements Performer<EmployeesAppModel> {
private final Random random = new Random();
@Override
public void perform(EmployeesAppModel application) throws Exception {
SwingEntityModel departmentModel = application.entityModel(Department.TYPE);
selectRandomRow(departmentModel.tableModel());
SwingEntityModel employeeModel = departmentModel.detailModel(Employee.TYPE);
if (employeeModel.tableModel().getRowCount() > 0) {
EntityConnection connection = employeeModel.connection();
connection.startTransaction();
try {
selectRandomRow(employeeModel.tableModel());
Entity selected = employeeModel.tableModel().selectionModel().getSelectedItem();
randomize(application.entities(), selected, null);
employeeModel.editModel().set(selected);
employeeModel.editModel().update();
selectRandomRow(employeeModel.tableModel());
selected = employeeModel.tableModel().selectionModel().getSelectedItem();
randomize(application.entities(), selected, null);
employeeModel.editModel().set(selected);
employeeModel.editModel().update();
}
finally {
if (random.nextBoolean()) {
connection.rollbackTransaction();
}
else {
connection.commitTransaction();
}
}
}
}
}
7. Full Demo Source
7.1. Domain
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.domain;
import is.codion.common.format.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.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.plugin.jasperreports.JRReportType;
import is.codion.plugin.jasperreports.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.sequence;
import static is.codion.framework.domain.entity.OrderBy.ascending;
import static is.codion.plugin.jasperreports.JasperReports.classPathReport;
import static java.util.Arrays.asList;
// This class contains the specification for the Employees application domain model
public final class Employees extends DefaultDomain {
// The domain type identifying this domain model
public static final DomainType DOMAIN = domainType(Employees.class);
// Entity type for the table employees.department
public interface Department {
EntityType TYPE = DOMAIN.entityType("employees.department");
// Columns for the columns in the employees.department table
Column<Integer> DEPARTMENT_NO = TYPE.integerColumn("department_no");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> LOCATION = TYPE.stringColumn("location");
}
// Entity type for the table employees.employee
public interface Employee {
EntityType TYPE = DOMAIN.entityType("employees.employee");
// Columns for the columns in the employees.employee table
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> JOB = TYPE.stringColumn("job");
Column<Integer> MANAGER_ID = TYPE.integerColumn("manager_id");
Column<LocalDate> HIREDATE = TYPE.localDateColumn("hiredate");
Column<BigDecimal> SALARY = TYPE.bigDecimalColumn("salary");
Column<Double> COMMISSION = TYPE.doubleColumn("commission");
Column<Integer> DEPARTMENT = TYPE.integerColumn("department_no");
// Foreign key attribute for the DEPTNO column in the table employees.employee
ForeignKey DEPARTMENT_FK = TYPE.foreignKey("department_no_fk", DEPARTMENT, Department.DEPARTMENT_NO);
// Foreign key attribute for the MGR column in the table employees.employee
ForeignKey MANAGER_FK = TYPE.foreignKey("manager_fk", MANAGER_ID, 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"), item("Clerk"),
item("Manager"), item("President"),
item("Salesman"));
}
// Initializes this domain model
public Employees() {
super(DOMAIN);
add(department(), employee());
add(Employee.EMPLOYEE_REPORT, classPathReport(Employees.class, "employees.jasper"));
}
EntityDefinition.Builder department() {
// Defining the entity Department.TYPE
return Department.TYPE.define(
Department.DEPARTMENT_NO.define()
.primaryKey()
.caption("No.")
.nullable(false),
Department.NAME.define()
.column()
.caption("Name")
.maximumLength(14)
.nullable(false),
Department.LOCATION.define()
.column()
.caption("Location")
.maximumLength(13))
.smallDataset(true)
.orderBy(ascending(Department.NAME))
.stringFactory(Department.NAME)
.caption("Department");
}
EntityDefinition.Builder employee() {
// Defining the entity Employee.TYPE
return Employee.TYPE.define(
Employee.ID.define()
.primaryKey(),
Employee.NAME.define()
.column()
.caption("Name")
.searchable(true)
.maximumLength(10)
.nullable(false),
Employee.DEPARTMENT.define()
.column()
.nullable(false),
Employee.DEPARTMENT_FK.define()
.foreignKey()
.caption("Department"),
Employee.JOB.define()
.column()
.caption("Job")
.items(Employee.JOB_VALUES),
Employee.SALARY.define()
.column()
.caption("Salary")
.nullable(false)
.valueRange(900, 10000)
.maximumFractionDigits(2),
Employee.COMMISSION.define()
.column()
.caption("Commission")
.valueRange(100, 2000)
.maximumFractionDigits(2),
Employee.MANAGER_ID.define()
.column(),
Employee.MANAGER_FK.define()
.foreignKey()
.caption("Manager"),
Employee.HIREDATE.define()
.column()
.caption("Hiredate")
.nullable(false)
.localeDateTimePattern(LocaleDateTimePattern.builder()
.delimiterDash()
.yearFourDigits()
.build()),
Employee.DEPARTMENT_LOCATION.define()
.denormalized(Employee.DEPARTMENT_FK, Department.LOCATION)
.caption("Location"))
.keyGenerator(sequence("employees.employee_seq"))
.orderBy(ascending(Employee.DEPARTMENT, Employee.NAME))
.stringFactory(Employee.NAME)
.caption("Employee")
.backgroundColorProvider((entity, attribute) -> {
if (attribute.equals(Employee.JOB) && "Manager".equals(entity.get(Employee.JOB))) {
return Color.CYAN;
}
return null;
})
.foregroundColorProvider((entity, attribute) -> {
if (attribute.equals(Employee.SALARY) && entity.get(Employee.SALARY).doubleValue() < 1300) {
return Color.RED;
}
return null;
});
}
}
7.2. Domain unit test
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.domain;
import is.codion.framework.demos.employees.domain.Employees.Department;
import is.codion.framework.demos.employees.domain.Employees.Employee;
import is.codion.framework.domain.entity.test.EntityTestUnit;
import org.junit.jupiter.api.Test;
public class EmployeesTest extends EntityTestUnit {
public EmployeesTest() {
super(new Employees());
}
@Test
void department() throws Exception {
test(Department.TYPE);
}
@Test
void employee() throws Exception {
test(Employee.TYPE);
}
}
7.3. Model
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.model;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.employees.domain.Employees.Employee;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.component.EntityComboBoxModel;
import java.util.Objects;
public final class EmployeeEditModel extends SwingEntityEditModel {
public EmployeeEditModel(EntityConnectionProvider connectionProvider) {
super(Employee.TYPE, connectionProvider);
initializeComboBoxModels(Employee.MANAGER_FK, Employee.DEPARTMENT_FK);
bindEvents();
}
// Providing a custom ComboBoxModel for the manager attribute, which only shows managers and the president
@Override
public EntityComboBoxModel createForeignKeyComboBoxModel(ForeignKey foreignKey) {
EntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKey);
if (foreignKey.equals(Employee.MANAGER_FK)) {
//Customize the null value caption so that it displays 'None'
//instead of the default '-' character
comboBoxModel.setNullCaption("None");
//we do not want filtering to remove a value that is selected
//and thereby change the selection, see bindEvents() below
comboBoxModel.filterSelectedItem().set(false);
//Only select the president and managers from the database
comboBoxModel.condition().set(() ->
Employee.JOB.in("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
insertUpdateOrDeleteEvent().addListener(() -> foreignKeyComboBoxModel(Employee.MANAGER_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
valueEvent(Employee.DEPARTMENT_FK).addConsumer(department -> {
//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
foreignKeyComboBoxModel(Employee.MANAGER_FK).includeCondition().set(manager ->
Objects.equals(manager.entity(Employee.DEPARTMENT_FK), department)
&& !Objects.equals(manager, entity()));
});
}
}
7.4. UI
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.ui;
import is.codion.framework.demos.employees.domain.Employees.Department;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import static is.codion.swing.common.ui.component.Components.borderLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
public class DepartmentEditPanel extends EntityEditPanel {
public DepartmentEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(Department.DEPARTMENT_NO);
createTextField(Department.DEPARTMENT_NO)
.columns(3)
//don't allow editing of existing department numbers
.enabled(editModel().exists().not());
createTextField(Department.NAME)
.columns(8);
createTextField(Department.LOCATION)
.columns(12);
editModel().exists().addConsumer(exists ->
initialFocusAttribute().set(exists ? Department.NAME : Department.DEPARTMENT_NO));
setLayout(borderLayout());
add(borderLayoutPanel()
.northComponent(borderLayoutPanel()
.westComponent(createInputPanel(Department.DEPARTMENT_NO))
.centerComponent(createInputPanel(Department.NAME))
.build())
.centerComponent(createInputPanel(Department.LOCATION))
.build(), borderLayout().CENTER);
}
}
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.ui;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.report.ReportException;
import is.codion.framework.demos.employees.domain.Employees.Department;
import is.codion.framework.demos.employees.domain.Employees.Employee;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.swing.JRViewer;
import java.awt.Dimension;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class DepartmentTablePanel extends EntityTablePanel {
public DepartmentTablePanel(SwingEntityTableModel tableModel) {
super(tableModel);
}
@Override
protected void configureControls() {
control(TableControl.PRINT).set(Control.builder(this::viewEmployeeReport)
.name("Employee Report")
.enabled(tableModel().selectionModel().selectionNotEmpty())
.build());
}
private void viewEmployeeReport() throws ReportException, DatabaseException {
Collection<Integer> departmentNumbers =
Entity.distinct(Department.DEPARTMENT_NO,
tableModel().selectionModel().getSelectedItems());
Map<String, Object> reportParameters = new HashMap<>();
reportParameters.put("DEPTNO", departmentNumbers);
JasperPrint employeeReport = tableModel().connection()
.report(Employee.EMPLOYEE_REPORT, reportParameters);
Dialogs.componentDialog(new JRViewer(employeeReport))
.owner(this)
.modal(false)
.size(new Dimension(800, 600))
.show();
}
}
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.ui;
import is.codion.framework.demos.employees.domain.Employees.Department;
import is.codion.framework.demos.employees.domain.Employees.Employee;
import is.codion.swing.common.ui.component.Components;
import is.codion.swing.common.ui.component.text.NumberField;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.component.EntityComboBox;
import javax.swing.JPanel;
import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.flexibleGridLayout;
public class EmployeeEditPanel extends EntityEditPanel {
public EmployeeEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(Employee.NAME);
createTextField(Employee.NAME)
.columns(8);
createItemComboBox(Employee.JOB);
createForeignKeyComboBox(Employee.MANAGER_FK);
createTextField(Employee.SALARY)
.columns(5);
createTextField(Employee.COMMISSION)
.columns(5);
createTemporalFieldPanel(Employee.HIREDATE)
.columns(6);
setLayout(flexibleGridLayout(0, 3));
addInputPanel(Employee.NAME);
addInputPanel(Employee.DEPARTMENT_FK, createDepartmentPanel());
addInputPanel(Employee.JOB);
addInputPanel(Employee.MANAGER_FK);
add(gridLayoutPanel(1, 2)
.add(createInputPanel(Employee.SALARY))
.add(createInputPanel(Employee.COMMISSION))
.build());
addInputPanel(Employee.HIREDATE);
}
private JPanel createDepartmentPanel() {
EntityComboBox departmentBox = createForeignKeyComboBox(Employee.DEPARTMENT_FK).build();
NumberField<Integer> departmentNumberField = departmentBox.integerSelectorField(Department.DEPARTMENT_NO)
.transferFocusOnEnter(true)
.onBuild(field -> addValidator(Employee.DEPARTMENT, field))
.build();
component(Employee.DEPARTMENT_FK).set(departmentNumberField);
return Components.borderLayoutPanel()
.westComponent(departmentNumberField)
.centerComponent(departmentBox)
.build();
}
}
/*
* This file is part of Codion.
*
* Codion is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.employees.ui;
import is.codion.common.user.User;
import is.codion.framework.demos.employees.domain.Employees;
import is.codion.framework.demos.employees.domain.Employees.Department;
import is.codion.framework.demos.employees.domain.Employees.Employee;
import is.codion.framework.demos.employees.model.EmployeesAppModel;
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.common.ui.dialog.Dialogs;
import is.codion.swing.common.ui.laf.LookAndFeelProvider;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.EntityTablePanel;
import is.codion.swing.framework.ui.TabbedDetailLayout;
import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.io.File;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import static is.codion.framework.json.domain.EntityObjectMapper.entityObjectMapper;
import static java.util.Collections.singletonList;
public class EmployeesAppPanel extends EntityApplicationPanel<EmployeesAppModel> {
private static final String DEFAULT_FLAT_LOOK_AND_FEEL = "com.formdev.flatlaf.intellijthemes.FlatArcIJTheme";
public EmployeesAppPanel(EmployeesAppModel applicationModel) {
super(applicationModel);
}
@Override
protected List<EntityPanel> createEntityPanels() {
SwingEntityModel departmentModel = applicationModel().entityModel(Department.TYPE);
SwingEntityModel employeeModel = departmentModel.detailModel(Employee.TYPE);
EntityPanel employeePanel = new EntityPanel(employeeModel,
new EmployeeEditPanel(employeeModel.editModel()));
EntityPanel departmentPanel = new EntityPanel(departmentModel,
new DepartmentEditPanel(departmentModel.editModel()),
new DepartmentTablePanel(departmentModel.tableModel()),
config -> config.detailLayout(entityPanel -> TabbedDetailLayout.builder(entityPanel)
.splitPaneResizeWeight(0.4)
.build()));
departmentPanel.addDetailPanel(employeePanel);
return singletonList(departmentPanel);
}
public void importJSON() throws Exception {
File file = Dialogs.fileSelectionDialog()
.owner(this)
.fileFilter(new FileNameExtensionFilter("JSON files", "json"))
.fileFilter(new FileNameExtensionFilter("Text files", "txt"))
.selectFile();
List<Entity> entities = entityObjectMapper(applicationModel().entities())
.deserializeEntities(String.join("\n", Files.readAllLines(file.toPath())));
SwingEntityTableModel tableModel = SwingEntityTableModel.tableModel(entities, applicationModel().connectionProvider());
tableModel.editModel().readOnly().set(true);
EntityTablePanel tablePanel = new EntityTablePanel(tableModel,
config -> config.includePopupMenu(false));
Dialogs.componentDialog(tablePanel.initialize())
.owner(this)
.title("Import")
.show();
}
@Override
protected Controls createToolsMenuControls() {
return super.createToolsMenuControls()
.add(Control.builder(this::importJSON)
.name("Import JSON")
.build());
}
public static void main(String[] args) {
Arrays.stream(FlatAllIJThemes.INFOS)
.forEach(LookAndFeelProvider::addLookAndFeelProvider);
EntityPanel.Config.TOOLBAR_CONTROLS.set(true);
EntityApplicationPanel.builder(EmployeesAppModel.class, EmployeesAppPanel.class)
.applicationName("Employees")
.domainType(Employees.DOMAIN)
.defaultLookAndFeelClassName(DEFAULT_FLAT_LOOK_AND_FEEL)
.defaultLoginUser(User.parse("scott:tiger"))
.start();
}
}