Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.
-
Domain modelling in plain Java code without annotations.
-
Integrated JUnit testing of the domain model.
-
A minimal but complete JDBC abstraction layer.
-
Assembling a Swing UI based on the domain model is very straight forward.
-
Swing client provides:
-
A practically mouse free user experience.
-
Extensive searching capabilities.
-
-
Clients use either a local JDBC connection or are served by a featherweight RMI/HTTP server.
-
Integrated JasperReports support.
-
Rapid prototyping.
My main motivation for developing Codion back in 2004 was the lack of application frameworks based on Java Standard Edition. I was writing rather basic desktop CRUD appliations, so I wanted to stick with Standard Edition components, Swing, JDBC and RMI.
I figured a CRUD application framework should:
-
Embody Alan Kay’s adage "simple things should be simple, complex things should be possible".
-
Provide a reasonable set of application functionality out of the box.
-
Have a clear separation between model and UI for easy unit testing.
-
Limit accidental complexity and be intuitive and enjoyable to use.
Snapshots are available in Sonatype’s snapshots repository.
repositories {
maven {
url "https://s01.oss.sonatype.org/content/repositories/snapshots/"
}
}
The core Codion framework components use a limited set of third-party libraries, a Swing client with local JDBC and RMI connection capabilities pulls in the following dependencies:
-
org.json:json for persisting application preferences
-
org.kordamp.ikonli:ikonli-swing for application icon handling
-
io.github.hakky54:sslcontext-kickstart for RMI truststore handling
-
org.slf4j:slf4j-api for your logging framework of choice
-
JDBC driver
-
Logging framework
A basic CRUD client pulls in ~15 Codion modules totalling ~1.7MB, so the combined size of the Petclinic demo client for example, with local connection capabilities, is ~5MB.
The following applications can be found in the demos
folder of the Codion project, but are also available in separate Git repositories as fully configured stand-alone Gradle projects.
All these projects contain jlink/jpackage configurations for packaging the application, server, server monitor and load-test, if applicable.
Look & Feel provided by Flat Look and Feel.
Minimalistic bare-bones project, with a local JDBC connection option. A good place to start.
Fully configured multi-module project, with separate client modules configured for JDBC, RMI and HTTP connection options.
Includes server and server monitor modules and jlink/jpackage configurations.
The Kitchen Sink demo, with lots of customization examples.
Fully configured multi-module project, with separate client modules configured for JDBC, RMI and HTTP connection options.
Includes load-test, server, and server monitor modules and jlink/jpackage configurations.
Note
|
The "waterfall" master/detail UI layout used in these demo applications is what the framework provides by default and can be customized at will. |
Module |
Artifact |
is.codion.framework.domain |
is.codion:codion-framework-domain:0.18.22 |
Codion is not an Object Relational Mapping based framework, instead the domain model is based on concepts from entity relationship diagrams, entities, attributes, columns and foreign keys, eliminating most of the problems associated with object-relational impedance mismatch.
The Codion framework is based around the Entity
class which represents a row in a table or query.
An Entity
maps Attributes
to their respective values and keeps track of values that have been modified since they were first set. Entity
instances are basically data transfer objects and are not managed by the framework.
For persistance see Persistance below.
// the domain model instance
Store store = new Store();
// a factory for Entity instances from this domain model
Entities entities = store.entities();
// instantiate and populate a new customer instance
Entity customer = entities.builder(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve values
String lastName = customer.get(Customer.LAST_NAME);
Boolean active = customer.get(Customer.ACTIVE);
// modify values
customer.put(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // true
System.out.println(customer.original(Customer.LAST_NAME)); // "Doe"
// revert changes
customer.revert();
System.out.println(customer.modified()); //false
EntityType
represents a table (or query), Attribute
represents a typed value identifier, usually appearing as one of its subclasses Column
or ForeignKey
.
The metadata required to present and persist entities is encapsulated by EntityDefinition
and AttributeDefinition
.
In the below example, we define a domain model with two entities, Customer
and Address
with a master/detail retionship, using the following steps:
-
Extend the
DomainModel
class and create aDomainType
constant identifying the domain model. -
Create a namespace interface for each
Entity
and use theDomainType
to createEntityType
constants. -
Use the
EntityType
constant to createColumn
constants for each column and aForeignKey
constant for the foreign key relationship.- NOTE
-
The constants defined in the above steps represent the domain API and are usually all you need to work with the domain entities.
-
Use the
EntityType
constants to define each entity, based on attributes defined using theColumn
andForeignKey
constants, and add the entity definitions to the domain model.
import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.KeyGenerator.identity;
// Extend the DomainModel class.
public class Store extends DomainModel {
// Create a DomainType constant identifying the domain model.
public static final DomainType DOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.
public interface Customer {
// Use the DomainType and the table name to create an
// EntityType constant identifying the entity.
EntityType TYPE = DOMAIN.entityType("store.customer");
// Use the EntityType to create typed Column constants for each column.
Column<Long> ID = TYPE.longColumn("id");
Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
Column<String> LAST_NAME = TYPE.stringColumn("last_name");
Column<String> EMAIL = TYPE.stringColumn("email");
Column<Boolean> ACTIVE = TYPE.booleanColumn("active");
}
// Create a namespace interface for the Address entity.
public interface Address {
EntityType TYPE = DOMAIN.entityType("store.address");
Column<Long> ID = TYPE.longColumn("id");
Column<Long> CUSTOMER_ID = TYPE.longColumn("customer_id");
Column<String> STREET = TYPE.stringColumn("street");
Column<String> CITY = TYPE.stringColumn("city");
// Use the EntityType to create a ForeignKey
// constant for the foreign key relationship.
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
public Store() {
super(DOMAIN);
// Use the Customer.TYPE constant to define a new entity,
// based on attributes defined using the Column constants.
// This entity definition is then added to the domain model.
add(Customer.TYPE.define( // returns EntityDefinition.Builder
Customer.ID.define()
.primaryKey(), // returns ColumnDefinition.Builder
Customer.FIRST_NAME.define()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.define()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.define()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.define()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.keyGenerator(identity())
.stringFactory(StringFactory.builder()
.value(Customer.LAST_NAME)
.text(", ")
.value(Customer.FIRST_NAME)
.build())
.caption("Customer")
.build());
// Use the Address.TYPE constant to define a new entity,
// based on attributes defined using the Column and ForeignKey constants.
// This entity definition is then added to the domain model.
add(Address.TYPE.define(
Address.ID.define()
.primaryKey(),
Address.CUSTOMER_ID.define()
.column()
.nullable(false),
Address.CUSTOMER_FK.define()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.define()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.define()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.keyGenerator(identity())
.stringFactory(StringFactory.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
|
IntelliJ IDEA live templates for working with domain models. |
Here’s one entity definition from above, pulled apart, with the ingredients exposed.
Display code
ColumnDefinition.Builder<Long, ?> id =
Address.ID.define()
.primaryKey();
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.define()
.column()
.nullable(false);
ForeignKeyDefinition.Builder customerFk =
Address.CUSTOMER_FK.define()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.define()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.define()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
KeyGenerator keyGenerator = KeyGenerator.identity();
Function<Entity, String> stringFactory = StringFactory.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinition.Builder address =
Address.TYPE.define(id, customerId, customerFk, street, city)
.keyGenerator(keyGenerator)
.stringFactory(stringFactory)
.caption("Address");
add(address);
Module |
Artifact |
is.codion.framework.domain.test |
is.codion:codion-framework-domain-test:0.18.22 |
The DomainTest
class provides a JUnit testing harness for the domain model.
The DomainTest.test(entityType)
method runs insert, select, update and delete on a randomly (or manually) generated entity instance, verifying the results.
public class StoreTest extends DomainTest {
public StoreTest() {
super(new Store());
}
@Test
void customer() throws Exception {
test(Customer.TYPE);
}
@Test
void address() throws Exception {
test(Address.TYPE);
}
}
Module |
Artifact |
is.codion.swing.framework.ui |
is.codion:codion-swing-framework-ui:0.18.22 |
In the following example, we use the domain model from above and implement a CustomerEditPanel
and AddressEditPanel
by extending EntityEditPanel
.
These edit panels, as their names suggest, provide the UI for editing entity instances.
In the main
method we use these building blocks to assemble and display a client.
public class StoreDemo {
private static class CustomerEditPanel extends EntityEditPanel {
private CustomerEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(Customer.FIRST_NAME);
createTextField(Customer.FIRST_NAME);
createTextField(Customer.LAST_NAME);
createTextField(Customer.EMAIL);
createCheckBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
private static class AddressEditPanel extends EntityEditPanel {
private AddressEditPanel(SwingEntityEditModel addressEditModel) {
super(addressEditModel);
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(Address.STREET);
createForeignKeyComboBox(Address.CUSTOMER_FK);
createTextField(Address.STREET);
createTextField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel(new FlatMaterialDarkerIJTheme());
Database database = H2DatabaseFactory
.createDatabase("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionProvider connectionProvider =
LocalEntityConnectionProvider.builder()
.database(database)
.domain(new Store())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModel customerModel =
new SwingEntityModel(Customer.TYPE, connectionProvider);
EntityPanel customerPanel =
new EntityPanel(customerModel,
new CustomerEditPanel(customerModel.editModel()));
SwingEntityModel addressModel =
new SwingEntityModel(Address.TYPE, connectionProvider);
EntityPanel addressPanel =
new EntityPanel(addressModel,
new AddressEditPanel(addressModel.editModel()));
customerModel.addDetailModel(addressModel);
customerPanel.addDetailPanel(addressPanel);
addressPanel.tablePanel()
.conditions().view().set(SIMPLE);
customerModel.tableModel().refresh();
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
SwingUtilities.invokeLater(() ->
Dialogs.componentDialog(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connectionProvider.close())
.show());
}
}
…and the result, all in all around 150 lines of code.
To run the above application, use the following Gradle task:
gradlew demo-manual:runStoreDemo
Module |
Artifact |
Description |
is.codion.framework.db.core |
is.codion:codion-framework-db-core:0.18.22 |
Core |
is.codion.framework.db.local |
is.codion:codion-framework-db-local:0.18.22 |
JDBC |
is.codion.framework.db.rmi |
is.codion:codion-framework-db-rmi:0.18.22 |
RMI |
is.codion.framework.db.http |
is.codion:codion-framework-db-http:0.18.22 |
HTTP |
The EntityConnection
interface defines the database layer.
There are three implementations available; local, which is based on a direct JDBC connection (used below), RMI and HTTP which are both served by the Codion Server.
Database database = H2DatabaseFactory
.createDatabase("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionProvider connectionProvider =
LocalEntityConnectionProvider.builder()
.database(database)
.domain(new Store())
.user(User.parse("scott:tiger"))
.build();
EntityConnection connection = connectionProvider.connection();
List<Entity> customersNamedDoe =
connection.select(Customer.LAST_NAME.equalTo("Doe"));
List<Entity> doesAddresses =
connection.select(Address.CUSTOMER_FK.in(customersNamedDoe));
List<Entity> customersWithoutEmail =
connection.select(Customer.EMAIL.isNull());
List<String> activeCustomerEmailAddresses =
connection.select(Customer.EMAIL,
Customer.ACTIVE.equalTo(true));
List<Entity> activeCustomersWithEmailAddresses =
connection.select(and(
Customer.ACTIVE.equalTo(true),
Customer.EMAIL.isNotNull()));
Entities entities = connection.entities();
Entity customer = entities.builder(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entity address = entities.builder(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.Key addressKey = connection.insert(address);
customer.put(Customer.EMAIL, "[email protected]");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();
The SQL queries generated by the framework are extremely simple, which means that the DBMS specific implementations are trivial and mostly concerned with primary key generation strategies and providing information on supported functionality.
DBMS |
Artifact |
Db2 |
is.codion:codion-dbms-db2:0.18.22 |
Derby |
is.codion:codion-dbms-derby:0.18.22 |
H2 |
is.codion:codion-dbms-h2:0.18.22 |
HSQL |
is.codion:codion-dbms-hsql:0.18.22 |
MariaDB |
is.codion:codion-dbms-mariadb:0.18.22 |
MySQL |
is.codion:codion-dbms-mysql:0.18.22 |
Oracle |
is.codion:codion-dbms-oracle:0.18.22 |
PostgreSQL |
is.codion:codion-dbms-postgresql:0.18.22 |
SQLite |
is.codion:codion-dbms-sqlite:0.18.22 |
SQL Server |
is.codion:codion-dbms-sqlserver:0.18.22 |
The Oracle, PostgreSQL and H2 implementations have all been used in production systems for many years, whereas the Db2 and SQL Server implementations have only been used for testing purposes. The rest have not been formally tested, but chances are they will just work.
Localized messages are available in English (default) and Icelandic. There are a whole lot of localized messages so if you are interested in providing translations that would be much appreciated. This i18n page can be generated with the following Gradle target.
gradlew documentation:generateI18nPage
The primary reason for the 0.x.y version is to be able to respond to community feedback before freezing the public API. Until version 1.0, backwards compatibility will not be a priority and the API should be considered unstable. All changes will be documented in the Change Log and upgrade instructions included when necessary.
After version 1.0 the plan is to use Semantic Versioning.
For copyright and managament overhead reasons, code contributions will not be accepted at this time.
Help with translations is very much appreciated though.
Bug reports are truly appreciated, please report bugs via issues.
Feel free to discuss features, design, API and anything Codion related.
For more information: Codion Website.