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 INT NOT NULL,
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),
constraint employee_job_chk check (job between 1 and 5)
);
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', 1, NULL, '1981-11-17', 5000, NULL, 10),
(3, 'Jones', 2, 8, '1981-04-02', 2975, NULL, 20),
(5, 'Blake', 2, 8, '1981-05-01', 2850, NULL, 30),
(1, 'Allen', 4, 5, '1981-02-20', 1600, 300, 30),
(2, 'Ward', 4, 5, '1981-02-22', 1250, 500, 30),
(4, 'Martin', 4, 5, '1981-09-28', 1250, 1400, 30),
(6, 'Clark', 2, 8, '1981-06-09', 2450, 1500, 10),
(7, 'Scott', 3, 3, '1987-04-19', 3000, 1500, 20),
(9, 'Turner', 4, 5, '1981-09-08', 1500, 0, 30),
(10, 'Adams', 5, 3, '1988-05-15', 1100, NULL, 20),
(11, 'James', 5, 6, '1996-10-03', 950, 1500, 10),
(12, 'Ford', 5, 3, '1988-12-12', 3200, 1200, 20),
(13, 'Miller', 5, 6, '1983-01-23', 1300, 1200, 10),
(14, 'Ben', 5, 3, '1989-12-12', 1600, 1200, 20),
(15, 'Baker', 5, 6, '2007-01-01', 1000, NULL, 10),
(16, 'Trevor', 5, 6, '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 DomainModel 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 DomainModel {
// 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<Integer> JOB = TYPE.integerColumn("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");
// Constants for the allowed JOB values
int PRESIDENT = 1;
int MANAGER = 2;
int ANALYST = 3;
int SALESMAN = 4;
int CLERK = 5;
List<Item<Integer>> JOB_ITEMS = asList(
item(ANALYST, "Analyst"), item(CLERK, "Clerk"),
item(MANAGER, "Manager"), item(PRESIDENT, "President"),
item(SALESMAN, "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 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")
.build();
}
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 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_ITEMS),
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")
.build();
}
}
3.1. Domain unit test
To unit test the domain model we extend DomainTest and create a test method for each entity we want tested and within that method call test(entityType). The DomainTest 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 by passing our own EntityFactory implementation to the super constructor, overriding entity(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 DomainTest.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 DomainTest {
public EmployeesTest() {
super(new Employees());
}
@Test
void department() {
test(Department.TYPE);
}
@Test
void employee() {
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);
}
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 and prevents an employee from being made her own manager.
// Providing a custom ComboBoxModel for the manager attribute, which only shows managers and the president
@Override
public EntityComboBoxModel createForeignKeyComboBoxModel(ForeignKey foreignKey) {
if (foreignKey.equals(Employee.MANAGER_FK)) {
EntityComboBoxModel managerComboBoxModel = EntityComboBoxModel.builder(Employee.TYPE, connectionProvider())
//Customize the null value caption so that it displays 'None'
//instead of the default '-' character
.nullCaption("None")
//Only select the president and managers from the database
.condition(() -> Employee.JOB.in(Employee.MANAGER, Employee.PRESIDENT))
.build();
//Refresh the manager ComboBoxModel when an employee is added, deleted or updated,
//in case a new manager got hired, fired or promoted
afterInsertUpdateOrDelete().addListener(managerComboBoxModel::refresh);
//hide the employee being edited to prevent an employee from being made her own manager
entity().addConsumer(employee ->
managerComboBoxModel.filter().predicate().set(manager -> !Objects.equals(manager, employee)));
//and only show managers from the currently selected department
value(Employee.DEPARTMENT_FK).addConsumer(department ->
managerComboBoxModel.filter().get(Employee.DEPARTMENT_FK).set(department.primaryKey()));
return managerComboBoxModel;
}
return super.createForeignKeyComboBoxModel(foreignKey);
}
For further information on event binding see Manual#Event binding.
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().entity().exists().not());
createTextField(Department.NAME)
.columns(8);
createTextField(Department.LOCATION)
.columns(12);
editModel().entity().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() {
Collection<Integer> departmentNumbers =
Entity.distinct(Department.DEPARTMENT_NO,
tableModel().selection().items().get());
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 setupControls() to populate the print control with our custom report one.
@Override
protected void setupControls() {
control(PRINT).set(Control.builder()
.command(this::viewEmployeeReport)
.name("Employee Report")
.smallIcon(FrameworkIcons.instance().print())
.enabled(tableModel().selection().empty().not())
.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();
}
}
We extend EntityTablePanel for the employee entity in order to provide data specific cell coloring.
/*
* 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) 2024, Björn Darri Sigurðsson.
*/
package is.codion.demos.employees.ui;
import is.codion.demos.employees.domain.Employees.Employee;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTableCellRenderer;
import is.codion.swing.framework.ui.EntityTablePanel;
import java.awt.Color;
public class EmployeeTablePanel extends EntityTablePanel {
public EmployeeTablePanel(SwingEntityTableModel tableModel) {
super(tableModel, config -> config
.cellRenderer(Employee.JOB, EntityTableCellRenderer.builder(Employee.JOB, tableModel)
.background((table, employee, attribute, job) ->
"Manager".equals(job) ? Color.CYAN : null)
.build())
.cellRenderer(Employee.SALARY, EntityTableCellRenderer.builder(Employee.SALARY, tableModel)
.foreground((table, employee, attribute, salary) ->
salary.doubleValue() < 1300 ? Color.RED : null)
.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()),
new EmployeeTablePanel(employeeModel.tableModel()));
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 List.of(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 IOException {
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 = new SwingEntityTableModel(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 Optional<Controls> createToolsMenuControls() {
return super.createToolsMenuControls()
.map(controls -> controls.copy()
.control(Control.builder()
.command(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::addLookAndFeel);
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)
.clientType(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) {
SwingEntityModel departmentModel = application.entityModel(Department.TYPE);
departmentModel.editModel().entity().set(new DefaultEntityFactory(application.connection()).entity(Department.TYPE));
departmentModel.editModel().insert();
}
}
public final class InsertEmployee extends AbstractPerformer {
@Override
public void perform(EmployeesAppModel application) {
SwingEntityModel departmentModel = application.entityModel(Department.TYPE);
selectRandomRow(departmentModel.tableModel());
SwingEntityModel employeeModel = departmentModel.detailModel(Employee.TYPE);
Entity employee = new DefaultEntityFactory(application.connection()).entity(Employee.TYPE);
employee.put(Employee.DEPARTMENT_FK, departmentModel.tableModel().selection().item().get());
employeeModel.editModel().entity().set(employee);
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 extends AbstractPerformer {
@Override
public void perform(EmployeesAppModel application) {
selectRandomRow(application.entityModel(Department.TYPE).tableModel());
}
}
public final class UpdateEmployee extends AbstractPerformer {
private final Random random = new Random();
@Override
public void perform(EmployeesAppModel application) {
SwingEntityModel departmentModel = application.entityModel(Department.TYPE);
selectRandomRow(departmentModel.tableModel());
SwingEntityModel employeeModel = departmentModel.detailModel(Employee.TYPE);
EntityFactory entityFactory = new DefaultEntityFactory(application.connection());
if (employeeModel.tableModel().items().visible().count() > 0) {
EntityConnection connection = employeeModel.connection();
connection.startTransaction();
try {
selectRandomRow(employeeModel.tableModel());
Entity selected = employeeModel.tableModel().selection().item().get();
entityFactory.modify(selected);
employeeModel.editModel().entity().set(selected);
employeeModel.editModel().update();
selectRandomRow(employeeModel.tableModel());
selected = employeeModel.tableModel().selection().item().get();
entityFactory.modify(selected);
employeeModel.editModel().entity().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.demos.employees.domain;
import is.codion.common.format.LocaleDateTimePattern;
import is.codion.common.item.Item;
import is.codion.framework.domain.DomainModel;
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.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 DomainModel {
// 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<Integer> JOB = TYPE.integerColumn("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");
// Constants for the allowed JOB values
int PRESIDENT = 1;
int MANAGER = 2;
int ANALYST = 3;
int SALESMAN = 4;
int CLERK = 5;
List<Item<Integer>> JOB_ITEMS = asList(
item(ANALYST, "Analyst"), item(CLERK, "Clerk"),
item(MANAGER, "Manager"), item(PRESIDENT, "President"),
item(SALESMAN, "Salesman"));
}
// Initializes this domain model
public Employees() {
super(DOMAIN);
add(department(), employee());
add(Employee.EMPLOYEE_REPORT, classPathReport(Employees.class, "employees.jasper"));
}
EntityDefinition 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")
.build();
}
EntityDefinition 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_ITEMS),
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")
.build();
}
}
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.demos.employees.domain;
import is.codion.demos.employees.domain.Employees.Department;
import is.codion.demos.employees.domain.Employees.Employee;
import is.codion.framework.domain.test.DomainTest;
import org.junit.jupiter.api.Test;
public class EmployeesTest extends DomainTest {
public EmployeesTest() {
super(new Employees());
}
@Test
void department() {
test(Department.TYPE);
}
@Test
void employee() {
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.demos.employees.model;
import is.codion.demos.employees.domain.Employees.Employee;
import is.codion.framework.db.EntityConnectionProvider;
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);
}
// Providing a custom ComboBoxModel for the manager attribute, which only shows managers and the president
@Override
public EntityComboBoxModel createForeignKeyComboBoxModel(ForeignKey foreignKey) {
if (foreignKey.equals(Employee.MANAGER_FK)) {
EntityComboBoxModel managerComboBoxModel = EntityComboBoxModel.builder(Employee.TYPE, connectionProvider())
//Customize the null value caption so that it displays 'None'
//instead of the default '-' character
.nullCaption("None")
//Only select the president and managers from the database
.condition(() -> Employee.JOB.in(Employee.MANAGER, Employee.PRESIDENT))
.build();
//Refresh the manager ComboBoxModel when an employee is added, deleted or updated,
//in case a new manager got hired, fired or promoted
afterInsertUpdateOrDelete().addListener(managerComboBoxModel::refresh);
//hide the employee being edited to prevent an employee from being made her own manager
entity().addConsumer(employee ->
managerComboBoxModel.filter().predicate().set(manager -> !Objects.equals(manager, employee)));
//and only show managers from the currently selected department
value(Employee.DEPARTMENT_FK).addConsumer(department ->
managerComboBoxModel.filter().get(Employee.DEPARTMENT_FK).set(department.primaryKey()));
return managerComboBoxModel;
}
return super.createForeignKeyComboBoxModel(foreignKey);
}
}
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.demos.employees.ui;
import is.codion.demos.employees.domain.Employees.Department;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import java.awt.BorderLayout;
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().entity().exists().not());
createTextField(Department.NAME)
.columns(8);
createTextField(Department.LOCATION)
.columns(12);
editModel().entity().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.demos.employees.ui;
import is.codion.demos.employees.domain.Employees.Department;
import is.codion.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 is.codion.swing.framework.ui.icon.FrameworkIcons;
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;
import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.PRINT;
public class DepartmentTablePanel extends EntityTablePanel {
public DepartmentTablePanel(SwingEntityTableModel tableModel) {
super(tableModel);
}
@Override
protected void setupControls() {
control(PRINT).set(Control.builder()
.command(this::viewEmployeeReport)
.name("Employee Report")
.smallIcon(FrameworkIcons.instance().print())
.enabled(tableModel().selection().empty().not())
.build());
}
private void viewEmployeeReport() {
Collection<Integer> departmentNumbers =
Entity.distinct(Department.DEPARTMENT_NO,
tableModel().selection().items().get());
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.demos.employees.ui;
import is.codion.demos.employees.domain.Employees.Department;
import is.codion.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.demos.employees.ui;
import is.codion.common.user.User;
import is.codion.demos.employees.domain.Employees;
import is.codion.demos.employees.domain.Employees.Department;
import is.codion.demos.employees.domain.Employees.Employee;
import is.codion.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.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static is.codion.framework.json.domain.EntityObjectMapper.entityObjectMapper;
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()),
new EmployeeTablePanel(employeeModel.tableModel()));
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 List.of(departmentPanel);
}
public void importJSON() throws IOException {
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 = new SwingEntityTableModel(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 Optional<Controls> createToolsMenuControls() {
return super.createToolsMenuControls()
.map(controls -> controls.copy()
.control(Control.builder()
.command(this::importJSON)
.name("Import JSON"))
.build());
}
public static void main(String[] args) {
Arrays.stream(FlatAllIJThemes.INFOS)
.forEach(LookAndFeelProvider::addLookAndFeel);
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();
}
}