Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Codion Application Framework

Codion logo

CILicense GNU%20GPL blueJava Compatability 21+codion swing framework ui?label=maven%20central&color=bluechat Github%20discussions blue

Introduction

Codion is a full-stack, Java rich client desktop CRUD application framework, based solely on Java Standard Edition components.

Motivation

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.

Download

Latest release (0.18.85)

Binaries are available on Maven Central.

Development version (0.18.86-SNAPSHOT)

Note
Snapshot versions are not automatically published, feel free to create an issue asking for a snapshot version.

Snapshots will be available in Sonatype’s snapshots repository.

repositories {
maven {
url "https://central.sonatype.com/repository/maven-snapshots/"
}
}

Dependencies

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:

Demo application projects

The three CRUD demo apps below 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.

A SDKMAN desktop app, demonstrating Swing UI development using the codion-swing-common-ui library for apps not requiring CRUD functionality.

UI design and SDKMAN API borrowed from sdkman-ui. This app would not exist without it!

SDKBOY client

A simple LLM chat app, mixing a custom UI for chat interaction with some basic CRUD functionality.

Includes modules configured for the OpenAI models as well as one configured for a local Ollama model. A module for running a local Ollama model using Testcontainers is included.

Llemmy client

Minimalistic bare-bones CRUD application project, with a local JDBC connection option. A good place to start.

Petclinic client

Fully configured multi-module CRUD application project, with separate client modules configured for JDBC, RMI and HTTP connection options.

Includes server and server monitor modules and jlink/jpackage configurations.

World client

The Kitchen Sink demo, with lots of customization and deployment examples.

Fully configured multi-module CRUD application 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.

Chinook client
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.

Domain model

Module

Artifact

is.codion.framework.domain

is.codion:codion-framework-domain:0.18.85

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.

Entities

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 persistence see Persistence below.

// the domain model instanceStorestore = newStore();
// a factory for Entity instances from this domain modelEntitiesentities = store.entities();
// instantiate and populate a new customer instanceEntitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.with(Customer.ACTIVE, true)
.build();
// retrieve valuesStringlastName = customer.get(Customer.LAST_NAME);
Booleanactive = customer.get(Customer.ACTIVE);
// modify valuescustomer.set(Customer.LAST_NAME, "Carter");
System.out.println(customer.modified()); // trueSystem.out.println(customer.original(Customer.LAST_NAME)); // "Doe"// revert changescustomer.revert();
System.out.println(customer.modified()); //false

Defining entities

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:

  1. Extend the DomainModel class and create a DomainType constant identifying the domain model.

  2. Create a namespace interface for each Entity and use the DomainType to create EntityType constants.

  3. Use the EntityType constant to create Column constants for each column and a ForeignKey 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.

  4. Use the EntityType constants to define each entity, based on attributes defined using the Column and ForeignKey constants, and add the entity definitions to the domain model.

importstaticis.codion.framework.domain.DomainType.domainType;
importstaticis.codion.framework.domain.entity.attribute.Column.Generator.identity;
// Extend the DomainModel class.publicclassStoreextendsDomainModel {
// Create a DomainType constant identifying the domain model.publicstaticfinalDomainTypeDOMAIN = domainType(Store.class);
// Create a namespace interface for the Customer entity.publicinterfaceCustomer {
// Use the DomainType and the table name to create an// EntityType constant identifying the entity.EntityTypeTYPE = 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.publicinterfaceAddress {
EntityTypeTYPE = 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.ForeignKeyCUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
publicStore() {
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.as()
.attributes( // returns EntityDefinition.BuilderCustomer.ID.as()
.primaryKey() // returns ColumnDefinition.Builder
.generator(identity()),
Customer.FIRST_NAME.as()
.column() // returns ColumnDefinition.Builder
.caption("First name")
.nullable(false)
.maximumLength(40),
Customer.LAST_NAME.as()
.column()
.caption("Last name")
.nullable(false)
.maximumLength(40),
Customer.EMAIL.as()
.column()
.caption("Email")
.maximumLength(100),
Customer.ACTIVE.as()
.column()
.caption("Active")
.nullable(false)
.defaultValue(true))
.formatter(EntityFormatter.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.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(identity()),
Address.CUSTOMER_ID.as()
.column()
.nullable(false),
Address.CUSTOMER_FK.as()
.foreignKey() // returns ForeignKeyDefinition.Builder
.caption("Customer"),
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100),
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50))
.formatter(EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build())
.caption("Address")
.build());
}
}
Note
IntelliJ IDEA live templates for working with domain models.

Entity definition expanded

Here’s one entity definition from above, pulled apart, with the ingredients exposed.

Display code
Generator<Long> generator = Generator.identity();
ColumnDefinition.Builder<Long, ?> id =
Address.ID.as()
.primaryKey()
.generator(generator);
ColumnDefinition.Builder<Long, ?> customerId =
Address.CUSTOMER_ID.as()
.column()
.nullable(false);
ForeignKeyDefinition.BuildercustomerFk =
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer");
ColumnDefinition.Builder<String, ?> street =
Address.STREET.as()
.column()
.caption("Street")
.nullable(false)
.maximumLength(100);
ColumnDefinition.Builder<String, ?> city =
Address.CITY.as()
.column()
.caption("City")
.nullable(false)
.maximumLength(50);
EntityFormatterformatter = EntityFormatter.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build();
EntityDefinitionaddress =
Address.TYPE.as()
.attributes(id, customerId, customerFk, street, city)
.formatter(formatter)
.caption("Address")
.build();
add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

is.codion:codion-framework-domain-test:0.18.85

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.

publicclassStoreTestextendsDomainTest {
publicStoreTest() {
super(newStore());
}
@Testvoidcustomer() {
test(Customer.TYPE);
}
@Testvoidaddress() {
test(Address.TYPE);
}
}

User interface

Module

Artifact

is.codion.swing.framework.ui

is.codion:codion-swing-framework-ui:0.18.85

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.

publicclassStoreDemo {
privatestaticclassCustomerEditPanelextendsEntityEditPanel {
privateCustomerEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().textField(Customer.FIRST_NAME);
create().textField(Customer.LAST_NAME);
create().textField(Customer.EMAIL);
create().checkBox(Customer.ACTIVE);
setLayout(gridLayout(4, 1));
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
addInputPanel(Customer.EMAIL);
addInputPanel(Customer.ACTIVE);
}
}
privatestaticclassAddressEditPanelextendsEntityEditPanel {
privateAddressEditPanel(SwingEntityEditModeleditModel) {
super(editModel);
}
@OverrideprotectedvoidinitializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
create().textField(Address.CITY);
setLayout(gridLayout(3, 1));
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
addInputPanel(Address.CITY);
}
}
publicstaticvoidmain(String[] args) throwsException {
UIManager.setLookAndFeel(newMaterialDarker());
Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:h2db",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModelcustomerModel =
newSwingEntityModel(Customer.TYPE, connection);
SwingEntityModeladdressModel =
newSwingEntityModel(Address.TYPE, connection);
customerModel.detail().add(addressModel);
EntityPanelcustomerPanel =
newEntityPanel(customerModel,
newCustomerEditPanel(customerModel.editModel()));
EntityPaneladdressPanel =
newEntityPanel(addressModel,
newAddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
addressPanel.tablePanel()
.condition().view().set(SIMPLE);
customerModel.tableModel().items().refresh();
SwingUtilities.invokeLater(() ->
Dialogs.builder()
.component(customerPanel.initialize())
.title("Customers")
.onClosed(e -> connection.close())
.show());
}
}

…​and the result, all in all around 150 lines of code.

customers

To run the above application, use the following Gradle task:

gradlew demo-manual:runStoreDemo

Persistence

Module

Artifact

Description

is.codion.framework.db

is.codion:codion-framework-db:0.18.85

Core

is.codion.framework.db.local

is.codion:codion-framework-db-local:0.18.85

JDBC

is.codion.framework.db.rmi

is.codion:codion-framework-db-rmi:0.18.85

RMI

is.codion.framework.db.http

is.codion:codion-framework-db-http:0.18.85

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.

Databasedatabase = H2DatabaseFactory
.create("jdbc:h2:mem:store",
"src/main/sql/create_schema_minimal.sql");
EntityConnectionconnection =
LocalEntityConnection.builder()
.database(database)
.domain(newStore())
.user(User.parse("scott:tiger"))
.build();
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()));
Entitiesentities = connection.entities();
Entitycustomer = entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Peter")
.with(Customer.LAST_NAME, "Jackson")
.build();
customer = connection.insertSelect(customer);
Entityaddress = entities.entity(Address.TYPE)
.with(Address.CUSTOMER_FK, customer)
.with(Address.STREET, "Elm st.")
.with(Address.CITY, "Boston")
.build();
Entity.KeyaddressKey = connection.insert(address);
customer.set(Customer.EMAIL, "mail@email.com");
customer = connection.updateSelect(customer);
connection.delete(List.of(addressKey, customer.primaryKey()));
connection.close();

Database support

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.85

Derby

is.codion:codion-dbms-derby:0.18.85

H2

is.codion:codion-dbms-h2:0.18.85

HSQL

is.codion:codion-dbms-hsql:0.18.85

MariaDB

is.codion:codion-dbms-mariadb:0.18.85

MySQL

is.codion:codion-dbms-mysql:0.18.85

Oracle

is.codion:codion-dbms-oracle:0.18.85

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.85

SQLite

is.codion:codion-dbms-sqlite:0.18.85

SQL Server

is.codion:codion-dbms-sqlserver:0.18.85

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, if not, create an issue, and we’ll figure it out.

Localization

Localized messages are available in English (default) and Icelandic. There are a 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

Versioning

Where is version 1.0?

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.

Semantic Versioning

After version 1.0 the plan is to use Semantic Versioning.

License

Codion is released under the Open Source GPLv3 license.

Keep in mind that you can freely use the GPL licensed version to create closed-source applications for personal or internal company use, since the license only kicks in when the application is distributed.

Open-source, not open-contribution

Pull requests

For copyright and managament overhead reasons, code contributions will not be accepted at this time.

See contributing.md for details.

Bug reports

Bug reports are truly appreciated, please report bugs via issues.

Discussions

Feel free to discuss features, design, API and anything Codion related.

For more information: Codion Website.

About

Codion Application Framework

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages