Skip to content

codion-is/codion

Repository files navigation

Codion Application Framework

Codion logo

CI License GNU%20GPL blue Java Compatability 17+ codion swing framework ui?label=maven%20central&color=blue chat 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.0)

Binaries are available on Maven Central.

Development version (0.18.1-SNAPSHOT)

Snapshots are available in Sonatype’s snapshots repository.

repositories {
    maven {
        url "https://s01.oss.sonatype.org/content/repositories/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:

A basic CRUD client pulls in ~15 Codion modules totalling ~1.7MB, so the combined size of the Petclinic demo client for example, with local connection capabilities, is ~5MB.

Demo application projects

The following applications can be found in the demos folder of the Codion project, but are also available in separate Git repositories as fully configured stand-alone Gradle projects.

All these projects contain jlink/jpackage configurations for packaging the application, server, server monitor and load-test, if applicable.

Look & Feel provided by Flat Look and Feel.

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

Petclinic client

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

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

World client

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

Includes load-test, server, and server monitor modules and jlink/jpackage configurations.

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

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 persistance see Persistance below.

// the domain model instance
Store store = new Store();

// a factory for Entity instances from this domain model
Entities entities = store.entities();

// instantiate and populate a new customer instance
Entity customer = entities.builder(Customer.TYPE)
        .with(Customer.FIRST_NAME, "John")
        .with(Customer.LAST_NAME, "Doe")
        .with(Customer.ACTIVE, true)
        .build();

// retrieve values
String lastName = customer.get(Customer.LAST_NAME);
Boolean active = customer.get(Customer.ACTIVE);

// modify values
customer.put(Customer.LAST_NAME, "Carter");

System.out.println(customer.modified()); // true
System.out.println(customer.original(Customer.LAST_NAME)); // "Doe"

// revert changes
customer.revert();

System.out.println(customer.modified()); //false

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.

import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.KeyGenerator.identity;

// Extend the DomainModel class.
public class Store extends DomainModel {

  // Create a DomainType constant identifying the domain model.
  public static final DomainType DOMAIN = domainType(Store.class);

  // Create a namespace interface for the Customer entity.
  public interface Customer {
    // Use the DomainType and the table name to create an
    // EntityType constant identifying the entity.
    EntityType TYPE = DOMAIN.entityType("store.customer");

    // Use the EntityType to create typed Column constants for each column.
    Column<Long> ID = TYPE.longColumn("id");
    Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
    Column<String> LAST_NAME = TYPE.stringColumn("last_name");
    Column<String> EMAIL = TYPE.stringColumn("email");
    Column<Boolean> ACTIVE = TYPE.booleanColumn("active");
  }

  // Create a namespace interface for the Address entity.
  public interface Address {
    EntityType TYPE = DOMAIN.entityType("store.address");

    Column<Long> ID = TYPE.longColumn("id");
    Column<Long> CUSTOMER_ID = TYPE.longColumn("customer_id");
    Column<String> STREET = TYPE.stringColumn("street");
    Column<String> CITY = TYPE.stringColumn("city");

    // Use the EntityType to create a ForeignKey
    // constant for the foreign key relationship.
    ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
  }

  public Store() {
    super(DOMAIN);
    // Use the Customer.TYPE constant to define a new entity,
    // based on attributes defined using the Column constants.
    // This entity definition is then added to the domain model.
    add(Customer.TYPE.define(                   // returns EntityDefinition.Builder
                    Customer.ID.define()
                            .primaryKey(),      // returns ColumnDefinition.Builder
                    Customer.FIRST_NAME.define()
                            .column()           // returns ColumnDefinition.Builder
                            .caption("First name")
                            .nullable(false)
                            .maximumLength(40),
                    Customer.LAST_NAME.define()
                            .column()
                            .caption("Last name")
                            .nullable(false)
                            .maximumLength(40),
                    Customer.EMAIL.define()
                            .column()
                            .caption("Email")
                            .maximumLength(100),
                    Customer.ACTIVE.define()
                            .column()
                            .caption("Active")
                            .nullable(false)
                            .defaultValue(true))
            .keyGenerator(identity())
            .stringFactory(StringFactory.builder()
                    .value(Customer.LAST_NAME)
                    .text(", ")
                    .value(Customer.FIRST_NAME)
                    .build())
            .caption("Customer"));

    // Use the Address.TYPE constant to define a new entity,
    // based on attributes defined using the Column and ForeignKey constants.
    // This entity definition is then added to the domain model.
    add(Address.TYPE.define(
                    Address.ID.define()
                            .primaryKey(),
                    Address.CUSTOMER_ID.define()
                            .column()
                            .nullable(false),
                    Address.CUSTOMER_FK.define()
                            .foreignKey()       // returns ForeignKeyDefinition.Builder
                            .caption("Customer"),
                    Address.STREET.define()
                            .column()
                            .caption("Street")
                            .nullable(false)
                            .maximumLength(100),
                    Address.CITY.define()
                            .column()
                            .caption("City")
                            .nullable(false)
                            .maximumLength(50))
            .keyGenerator(identity())
            .stringFactory(StringFactory.builder()
                    .value(Address.STREET)
                    .text(", ")
                    .value(Address.CITY)
                    .build())
            .caption("Address"));
  }
}
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
ColumnDefinition.Builder<Long, ?> id =
        Address.ID.define()
                .primaryKey();

ColumnDefinition.Builder<Long, ?> customerId =
        Address.CUSTOMER_ID.define()
                .column()
                .nullable(false);

ForeignKeyDefinition.Builder customerFk =
        Address.CUSTOMER_FK.define()
                .foreignKey()
                .caption("Customer");

ColumnDefinition.Builder<String, ?> street =
        Address.STREET.define()
                .column()
                .caption("Street")
                .nullable(false)
                .maximumLength(100);

ColumnDefinition.Builder<String, ?> city =
        Address.CITY.define()
                .column()
                .caption("City")
                .nullable(false)
                .maximumLength(50);

KeyGenerator keyGenerator = KeyGenerator.identity();

Function<Entity, String> stringFactory = StringFactory.builder()
        .value(Address.STREET)
        .text(", ")
        .value(Address.CITY)
        .build();

EntityDefinition.Builder address =
        Address.TYPE.define(id, customerId, customerFk, street, city)
                .keyGenerator(keyGenerator)
                .stringFactory(stringFactory)
                .caption("Address");

add(address);

Domain model test

Module

Artifact

is.codion.framework.domain.test

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

The EntityTestUnit class provides a JUnit testing harness for the domain model. The EntityTestUnit.test(entityType) method runs insert, select, update and delete on a randomly (or manually) generated entity instance, verifying the results.

public class StoreTest extends EntityTestUnit {

  public StoreTest() {
    super(new Store());
  }

  @Test
  void customer() throws Exception {
    test(Customer.TYPE);
  }

  @Test
  void address() throws Exception {
    test(Address.TYPE);
  }
}

User interface

Module

Artifact

is.codion.swing.framework.ui

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

In the following example, we use the domain model from above and implement a CustomerEditPanel and AddressEditPanel by extending EntityEditPanel. These edit panels, as their names suggest, provide the UI for editing entity instances. In the main method we use these building blocks to assemble and display a client.

public class StoreDemo {

  private static class CustomerEditPanel extends EntityEditPanel {

    private CustomerEditPanel(SwingEntityEditModel editModel) {
      super(editModel);
    }

    @Override
    protected void initializeUI() {
      initialFocusAttribute().set(Customer.FIRST_NAME);
      createTextField(Customer.FIRST_NAME);
      createTextField(Customer.LAST_NAME);
      createTextField(Customer.EMAIL);
      createCheckBox(Customer.ACTIVE);
      setLayout(gridLayout(4, 1));
      addInputPanel(Customer.FIRST_NAME);
      addInputPanel(Customer.LAST_NAME);
      addInputPanel(Customer.EMAIL);
      addInputPanel(Customer.ACTIVE);
    }
  }

  private static class AddressEditPanel extends EntityEditPanel {

    private AddressEditPanel(SwingEntityEditModel addressEditModel) {
      super(addressEditModel);
    }

    @Override
    protected void initializeUI() {
      initialFocusAttribute().set(Address.STREET);
      createForeignKeyComboBox(Address.CUSTOMER_FK);
      createTextField(Address.STREET);
      createTextField(Address.CITY);
      setLayout(gridLayout(3, 1));
      addInputPanel(Address.CUSTOMER_FK);
      addInputPanel(Address.STREET);
      addInputPanel(Address.CITY);
    }
  }

  public static void main(String[] args) throws Exception {
    UIManager.setLookAndFeel(new FlatMaterialDarkerIJTheme());

    Database database = H2DatabaseFactory
            .createDatabase("jdbc:h2:mem:h2db",
                    "src/main/sql/create_schema_minimal.sql");

    EntityConnectionProvider connectionProvider =
            LocalEntityConnectionProvider.builder()
                    .database(database)
                    .domain(new Store())
                    .user(User.parse("scott:tiger"))
                    .build();

    SwingEntityModel customerModel =
            new SwingEntityModel(Customer.TYPE, connectionProvider);
    EntityPanel customerPanel =
            new EntityPanel(customerModel,
                    new CustomerEditPanel(customerModel.editModel()));

    SwingEntityModel addressModel =
            new SwingEntityModel(Address.TYPE, connectionProvider);
    EntityPanel addressPanel =
            new EntityPanel(addressModel,
                    new AddressEditPanel(addressModel.editModel()));

    customerModel.addDetailModel(addressModel);
    customerPanel.addDetailPanel(addressPanel);

    addressPanel.tablePanel()
            .conditionPanelVisible().set(true);

    customerModel.tableModel().refresh();
    customerPanel.setBorder(createEmptyBorder(5, 5, 0, 5));
    customerPanel.initialize();

    Dialogs.componentDialog(customerPanel)
            .title("Customers")
            .onClosed(e -> connectionProvider.close())
            .show();
  }
}

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

customers

Persistance

Module

Artifact

Description

is.codion.framework.db.core

is.codion:codion-framework-db-core:0.18.0

Core

is.codion.framework.db.local

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

JDBC

is.codion.framework.db.rmi

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

RMI

is.codion.framework.db.http

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

HTTP

The EntityConnection interface defines the database layer. There are three implementations available; local, which is based on a direct JDBC connection (used below), RMI and HTTP which are both served by the Codion Server.

Database database = H2DatabaseFactory
        .createDatabase("jdbc:h2:mem:store",
                "src/main/sql/create_schema_minimal.sql");

EntityConnectionProvider connectionProvider =
        LocalEntityConnectionProvider.builder()
                .database(database)
                .domain(new Store())
                .user(User.parse("scott:tiger"))
                .build();

EntityConnection connection = connectionProvider.connection();

List<Entity> customersNamedDoe =
        connection.select(Customer.LAST_NAME.equalTo("Doe"));

List<Entity> doesAddresses =
        connection.select(Address.CUSTOMER_FK.in(customersNamedDoe));

List<Entity> customersWithoutEmail =
        connection.select(Customer.EMAIL.isNull());

List<String> activeCustomerEmailAddresses =
        connection.select(Customer.EMAIL,
                Customer.ACTIVE.equalTo(true));

List<Entity> activeCustomersWithEmailAddresses =
        connection.select(and(
                Customer.ACTIVE.equalTo(true),
                Customer.EMAIL.isNotNull()));

Entities entities = connection.entities();

Entity customer = entities.builder(Customer.TYPE)
        .with(Customer.FIRST_NAME, "Peter")
        .with(Customer.LAST_NAME, "Jackson")
        .build();

customer = connection.insertSelect(customer);

Entity address = entities.builder(Address.TYPE)
        .with(Address.CUSTOMER_FK, customer)
        .with(Address.STREET, "Elm st.")
        .with(Address.CITY, "Boston")
        .build();

Entity.Key addressKey = connection.insert(address);

customer.put(Customer.EMAIL, "[email protected]");

customer = connection.updateSelect(customer);

connection.delete(asList(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.0

Derby

is.codion:codion-dbms-derby:0.18.0

H2

is.codion:codion-dbms-h2:0.18.0

HSQL

is.codion:codion-dbms-hsql:0.18.0

MariaDB

is.codion:codion-dbms-mariadb:0.18.0

MySQL

is.codion:codion-dbms-mysql:0.18.0

Oracle

is.codion:codion-dbms-oracle:0.18.0

PostgreSQL

is.codion:codion-dbms-postgresql:0.18.0

SQLite

is.codion:codion-dbms-sqlite:0.18.0

SQL Server

is.codion:codion-dbms-sqlserver:0.18.0

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.

Localization

Localized messages are available in English (default) and Icelandic. There are a whole lot of localized messages so if you are interested in providing translations that would be much appreciated. This i18n page can be generated with the following Gradle target.

gradlew documentation:generateI18nPage

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.

See GPL FAQ

Open-source, not open-contribution

Pull requests

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

Help with translations is very much appreciated though.

Bug reports

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.