Note
Most of the code used throughout this manual is available in the manual demo module included in the project.

1. Framework

1.1. Domain & Database

1.1.1. Domain Model

A Codion domain model is based around the Entity interface, which represents a row in a table, providing access to the column values via its get() and put() methods.

Domain core classes
Domain

Specifies a domain model, containing entity definitions, procedures, functions and reports. A Codion domain model is implemented by extending the DomainModel class and populating it with entity definitions.

DomainType

A unique identifier for a domain model and a factory for EntityType instances associated with that domain model.

EntityType

A unique identifier for an entity type (based on a table or query) and a factory for Attribute instances associated with that entity type.

Attribute

A typed identifier for a column, foreign key or transient value, usually a Column or ForeignKey, allowing for type safe access to the associated value. Attributes are usually wrapped in an interface, serving as a convenient namespace.

attribute diagram
Column

An Attribute subclass representing a table column.

ForeignKey

An attribute subclass representing a foreign key relationship.

EntityDefinition

Encapsulates the meta-data required for presenting and persisting an entity.

AttributeDefinition

Each Attribute has an associated AttributeDefinition (or one of its subclasses) which encapsulates the meta-data required for presenting and persisting the associated value.

Entity

Represents a row in a table (or query) and maps Attributes to their associated values while keeping track of values which have been modified since they were initially set.

entity diagram
Entity.Key

Represents a unique key for a given entity.

Domain API

To define a domain model API we:

  • Create a DomainType constant representing the domain.

  • Use the DomainType to create EntityType constants for each table, wrapped in a namespace interface.

  • Use the EntityTypes to create Column constants for each column and a ForeignKey constant for each foreign key.

These constants represent the domain API and are used when referring to tables, columns or foreign keys.

public interface Store {

  DomainType DOMAIN = DomainType.domainType("Store");

  interface City {
    EntityType TYPE = DOMAIN.entityType("store.city"); //(1)

    Column<Integer> ID = TYPE.integerColumn("id"); //(2)
    Column<String> NAME = TYPE.stringColumn("name");
  }

  interface Customer {
    EntityType TYPE = DOMAIN.entityType("store.customer");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<Integer> CITY_ID = TYPE.integerColumn("city_id");

    ForeignKey CITY_FK = TYPE.foreignKey("city", CITY_ID, City.ID);
  }
}
  1. The DomainType instance serves as a factory for EntityTypes associated with that domain.

  2. Each EntityType instance serves as a factory for Columns and ForeignKeys associated with that entity.

Typically the underlying table name is used as the EntityType name, but you can use whatever identifying string you want and specify the table name via the tableName() builder method when defining the entity.

The underlying column name is typically used as the Column name, but as with the EntityType you can use whatever value you want and specify the column name via the columnName() method when creating the associated ColumnDefinition.

Domain implementation

The domain model is implemented by extending the DomainModel class and populating it with EntityDefinitions based on the domain tables. An EntityDefinition is comprised of AttributeDefinitions based on the Attributes associated with the entity.

The EntityType and Attribute constants provide define() methods returning builders which allow for further configuration (such as nullability and maximum length for values and the caption and primary key generator for the entity definition)..

Note
If a caption is not specified for an attribute, it is marked as hidden and is not visible in table views.
public static class StoreImpl extends DomainModel {

  public StoreImpl() {
    super(Store.DOMAIN); //(1)
    add(city(), customer());
  }

  EntityDefinition city() {
    return City.TYPE.define(
                    City.ID.define()
                            .primaryKey(),
                    City.NAME.define()
                            .column()
                            .caption("Name")
                            .nullable(false))
            .keyGenerator(KeyGenerator.identity())
            .caption("Cities")
            .build();
  }

  EntityDefinition customer() {
    return Customer.TYPE.define(
                    Customer.ID.define()
                            .primaryKey(),
                    Customer.NAME.define()
                            .column()
                            .caption("Name")
                            .maximumLength(42),
                    Customer.CITY_ID.define()
                            .column(),
                    Customer.CITY_FK.define()
                            .foreignKey()
                            .caption("City"))
            .keyGenerator(KeyGenerator.identity())
            .caption("Customers")
            .build();
  }
}
  1. The DomainType constant is a required constructor parameter.

Domain is a Service Provider Interface (SPI), and it is recommended to configure the domain implementation class for the Service Loader. Without the Service Loader you are restricted to a local JDBC connection, since you must manually provide a domain instance when establishing a connection, instead of just referring to the DomainType constant.

src/main/java/module-info.java

provides is.codion.framework.domain.Domain
          with is.codion.framework.demos.store.domain.StoreImpl;

or if not using Java Modules (JPMS)

src/main/resources/META-INF/services/is.codion.framework.domain.Domain

is.codion.framework.demos.store.domain.StoreImpl
Note
The domain model implementation must be on the classpath when running with a local JDBC connection, but when running with a RMI or HTTP connection the domain model API is sufficient. If you forsee using RMI or HTTP connections it is recommended to split your domain model into separate API and implementation modules, to simplify client configurations (see Chinook and World demo applications, see Petclinic for a simple single class domain model).

The domain model provides an Entities instance via entities(), which contains the entity definitions and serves as a factory for Entity and Entity.Key instances.

Domain store = new StoreImpl();

Entities entities = store.entities();

Entity city = entities.builder(City.TYPE)
        .with(City.NAME, "Reykjavík")
        .build();

Entity.Key customerKey = entities.keyBuilder(Customer.TYPE)
        .with(Customer.ID, 42)
        .build();

Entity customer = Entity.builder(customerKey)
        .with(Customer.NAME, "John")
        .with(Customer.CITY_FK, city)
        .build();

EntityDefinition customerDefinition = entities.definition(Customer.TYPE);

EntityDefinition cityDefinition = customerDefinition.foreignKeys().referencedBy(Customer.CITY_FK);

List<Column<?>> cityPrimaryKeyColumns = cityDefinition.primaryKey().columns();
Data type mapping
Java type SQL type

Short

java.sql.Types.SMALLINT

Integer

java.sql.Types.INTEGER

Double

java.sql.Types.DOUBLE

Long

java.sql.Types.BIGINT

BigDecimal

java.sql.Types.DECIMAL

LocalDateTime

java.sql.Types.TIMESTAMP

LocalDate

java.sql.Types.DATE

LocalTime

java.sql.Types.TIME

OffsetTime

java.sql.Types.TIME_WITH_TIMEZONE

OffsetDateTime

java.sql.Types.TIMESTAMP_WITH_TIMEZONE

java.util.Date

java.sql.Types.DATE

java.sql.Time

java.sql.Types.TIME

java.sql.Date

java.sql.Types.DATE

java.sql.Timestamp

java.sql.Types.TIMESTAMP

String

java.sql.Types.VARCHAR

Boolean

java.sql.Types.BOOLEAN

Character

java.sql.Types.CHAR

byte[]

java.sql.Types.BLOB

Foreign keys
Examples

A simple foreign key based on a single column.

ForeignKey CAPITAL_FK = TYPE.foreignKey("capital_fk", CAPITAL, City.ID);

Foreign key based on a composite key.

interface Parent {
  EntityType<Entity> TYPE = DOMAIN.entityType("parent");

  Column<Integer> ID_1 = TYPE.integerColumn("id1");
  Column<Integer> ID_2 = TYPE.integerColumn("id2");
}

interface Child {
  EntityType<Entity> TYPE = DOMAIN.entityType("child");

  Column<Integer> PARENT_ID_1 = TYPE.integerColumn("parent_id1");
  Column<Integer> PARENT_ID_2 = TYPE.integerColumn("parent_id2");

  ForeignKey PARENT_FK = TYPE.foreignKey("parent",
          PARENT_ID_1, Parent.ID_1,
          PARENT_ID_2, Parent.ID_2);
}

Domain constant definitions for the World demo application (simplified).

public interface World {

  DomainType DOMAIN = DomainType.domainType(World.class);

  interface City {
    EntityType TYPE = DOMAIN.entityType("world.city");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<String> COUNTRY_CODE = TYPE.stringColumn("countrycode");
    Column<String> DISTRICT = TYPE.stringColumn("district");
    Column<Integer> POPULATION = TYPE.integerColumn("population");

    ForeignKey COUNTRY_FK = TYPE.foreignKey("country_fk", COUNTRY_CODE, Country.CODE);
  }

  interface Country {
    EntityType TYPE = DOMAIN.entityType("world.country");

    Column<String> CODE = TYPE.stringColumn("code");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<String> CONTINENT = TYPE.stringColumn("continent");
    Column<String> REGION = TYPE.stringColumn("region");
    Column<Double> SURFACEAREA = TYPE.doubleColumn("surfacearea");
    Column<Integer> INDEPYEAR = TYPE.integerColumn("indepyear");
    Column<Integer> POPULATION = TYPE.integerColumn("population");
    Column<Double> LIFE_EXPECTANCY = TYPE.doubleColumn("lifeexpectancy");
    Column<Double> GNP = TYPE.doubleColumn("gnp");
    Column<Double> GNPOLD = TYPE.doubleColumn("gnpold");
    Column<String> LOCALNAME = TYPE.stringColumn("localname");
    Column<String> GOVERNMENTFORM = TYPE.stringColumn("governmentform");
    Column<String> HEADOFSTATE = TYPE.stringColumn("headofstate");
    Column<Integer> CAPITAL = TYPE.integerColumn("capital");
    Column<String> CODE_2 = TYPE.stringColumn("code2");
    Column<Integer> CAPITAL_POPULATION = TYPE.integerColumn("capital_population");
    Column<Integer> NO_OF_CITIES = TYPE.integerColumn("no_of_cities");
    Column<Integer> NO_OF_LANGUAGES = TYPE.integerColumn("no_of_languages");
    Column<byte[]> FLAG = TYPE.byteArrayColumn("flag");

    ForeignKey CAPITAL_FK = TYPE.foreignKey("capital_fk", CAPITAL, City.ID);
  }

  interface CountryLanguage {
    EntityType TYPE = DOMAIN.entityType("world.countrylanguage");

    Column<String> COUNTRY_CODE = TYPE.stringColumn("countrycode");
    Column<String> LANGUAGE = TYPE.stringColumn("language");
    Column<Boolean> IS_OFFICIAL = TYPE.booleanColumn("isofficial");
    Column<Double> PERCENTAGE = TYPE.doubleColumn("percentage");
    Column<Integer> NO_OF_SPEAKERS = TYPE.integerColumn("noOfSpeakers");

    ForeignKey COUNTRY_FK = TYPE.foreignKey("country_fk", COUNTRY_CODE, Country.CODE);
  }

  interface Continent {
    EntityType TYPE = DOMAIN.entityType("continent");

    Column<String> NAME = TYPE.stringColumn("continent");
    Column<Integer> SURFACE_AREA = TYPE.integerColumn("sum(surfacearea)");
    Column<Long> POPULATION = TYPE.longColumn("sum(population)");
    Column<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleColumn("min(lifeexpectancy)");
    Column<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleColumn("max(lifeexpectancy)");
    Column<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerColumn("min(indepyear)");
    Column<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerColumn("max(indepyear)");
    Column<Double> GNP = TYPE.doubleColumn("sum(gnp)");
  }
}
Attributes

For the framework to know how to present and persist values, Attributes need further configuration. Each attribute is represented by the AttributeDefinition class or one of its subclasses, which encapsulates the required metadata.

The Attribute, Column and ForeignKey classes provide methods for creating AttributeDefinition.Builder instances, which can be used to configure the attributes.

An Attribute can be configured three ways, as transient, derived or denormalized.

Transient

A transient attribute is not based on an underlying column, these attributes all have a default value of null and can be set and retrieved just like normal attributes.

Denormalized

An entity can include a read-only attribute value from an entity referenced via foreign key, by defining a denormalized attribute.

Country.CAPITAL_POPULATION.define()
        .denormalized(Country.CAPITAL_FK, City.POPULATION)
        .caption("Capital pop.")
        .numberFormatGrouping(true),
Derived

A derived attribute is used to represent a value which is derived from one or more attributes in the same entity. The value of a derived attribute is provided via a DerivedAttribute.Provider implementation as shown below.

CountryLanguage.NO_OF_SPEAKERS.define()
        .derived(new NoOfSpeakersProvider(),
                CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
        .caption("No. of speakers")
        .numberFormatGrouping(true),
final class NoOfSpeakersProvider implements DerivedAttribute.Provider<Integer> {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public Integer get(SourceValues values) {
    Double percentage = values.get(CountryLanguage.PERCENTAGE);
    Entity country = values.get(CountryLanguage.COUNTRY_FK);
    if (percentage != null && country != null && country.isNotNull(Country.POPULATION)) {
      return Double.valueOf(country.get(Country.POPULATION) * (percentage / 100)).intValue();
    }

    return null;
  }
}
Columns
Column

Column is used to represent attributes that are based on table columns.

Country.REGION.define()
        .column()
        .caption("Region")
        .nullable(false)
        .maximumLength(26),
Country.SURFACEAREA.define()
        .column()
        .caption("Surface area")
        .nullable(false)
        .numberFormatGrouping(true)
        .maximumFractionDigits(2),
Country.INDEPYEAR.define()
        .column()
        .caption("Indep. year")
        .valueRange(-2000, 2500),
Country.INDEPYEAR_SEARCHABLE.define()
        .column()
        .expression("to_char(indepyear)")
        .searchable(true)
        .readOnly(true),
Country.POPULATION.define()
        .column()
        .caption("Population")
        .nullable(false)
        .numberFormatGrouping(true),
Country.LIFE_EXPECTANCY.define()
        .column()
        .caption("Life expectancy")
        .maximumFractionDigits(1)
        .valueRange(0, 99),
Lazy loading

A column can be specified as being lazy, which means its value is not selected by default. Note that the selected entity contains a null value by default for lazy loaded values.

Country.FLAG.define()
        .column()
        .caption("Flag")
        .lazy(true),
Primary key

It is recommended that entities have a primary key defined, that is, one or more columns representing a unique combination.

The primary key defined in the domain model does not have to correspond to an actual table primary (or unique) key, although that is of course preferable.

If no primary key columns are specified, equals() will not work (since it is based on the primary key). You can still use the Entity.columnValuesEqual() method to check if all column based values are equal in two entities without primary keys.

Country.CODE.define()
        .primaryKey()
        .caption("Code")
        .updatable(true)
        .maximumLength(3),

In case of composite primary keys you simply specify the primary key index.

CountryLanguage.COUNTRY_CODE.define()
        .primaryKey(0)
        .updatable(true),
CountryLanguage.LANGUAGE.define()
        .primaryKey(1)
        .caption("Language")
        .updatable(true),
Subquery

A Column can represent a subquery returning a single value.

Country.NO_OF_CITIES.define()
        .subquery(
                "SELECT COUNT(*) FROM world.city " +
                        "WHERE city.countrycode = country.code")
        .caption("No. of cities"),
Boolean

For databases supporting Types.BOOLEAN you simply specify a column.

CountryLanguage.IS_OFFICIAL.define()
        .column()
        .caption("Official")
        .columnHasDefaultValue(true)
        .nullable(false),

For databases lacking native boolean support we can define a boolean column, specifying the underlying type and the true/false values.

Customer.ACTIVE.define()
        .booleanColumn("Is active", Integer.class, 1, 0)
Customer.ACTIVE.define()
        .booleanColumn("Is active", String.class, "true", "false")
Customer.ACTIVE.define()
        .booleanColumn("Is active", Character.class, 'T', 'F')

Note that boolean attributes always use the boolean Java type, the framework handles translating to and from the actual column values.

entity.put(Customer.ACTIVE, true);

Boolean isActive = entity.get(Customer.ACTIVE);
Item

A column based on a list of valid items.

private static final List<Item<String>> CONTINENT_ITEMS = asList(
        item("Africa"), item("Antarctica"), item("Asia"),
        item("Europe"), item("North America"), item("Oceania"),
        item("South America"));
Country.CONTINENT.define()
        .column()
        .items(CONTINENT_ITEMS)
        .caption("Continent")
        .nullable(false),
Foreign key

Foreign keys only have a single possible configuration.

Country.CAPITAL_FK.define()
        .foreignKey()
        .caption("Capital"),
Domain

Each entity is defined by creating a EntityDefinition.Builder instance via EntityType.define() and adding it to the domain model, via the add(EntityDefinition) or add(EntityDefinition.Builder) methods in the DomainModel class. The framework assumes the entityType name is the underlying table name, but the tableName can be specified via EntityDefinition.Builder.tableName(String) method.

EntityDefinition city() {
  return City.TYPE.define(
                  City.ID.define()
                          .primaryKey(),
                  City.NAME.define()
                          .column()
                          .caption("Name")
                          .searchable(true)
                          .nullable(false)
                          .maximumLength(35),
                  City.COUNTRY_CODE.define()
                          .column()
                          .nullable(false),
                  City.COUNTRY_FK.define()
                          .foreignKey()
                          .caption("Country"),
                  City.DISTRICT.define()
                          .column()
                          .caption("District")
                          .nullable(false)
                          .maximumLength(20),
                  City.POPULATION.define()
                          .column()
                          .caption("Population")
                          .nullable(false)
                          .numberFormatGrouping(true),
                  City.LOCATION.define()
                          .column()
                          .caption("Location")
                          .columnClass(String.class, new LocationConverter())
                          .comparator(new LocationComparator()))
          .keyGenerator(sequence("world.city_seq"))
          .validator(new CityValidator())
          .orderBy(ascending(City.NAME))
          .stringFactory(City.NAME)
          .description("Cities of the World")
          .caption("City")
          .build();
}
KeyGenerator

The framework provides implementations for most commonly used primary key generation strategies, identity column, sequence (with or without trigger) and auto-increment columns. The KeyGenerator class serves as a factory for KeyGenerator implementations. Static imports are assumed in the below examples.

Identity

Based on identity columns, supported by most DBMSs.

.keyGenerator(identity())
Automatic

This assumes the underlying primary key column is either an auto-increment column or is populated from a sequence using a trigger during insert. For auto-increment columns the valueSource parameter should be the table name and for a sequence/trigger it should be the sequence name.

//Auto increment column in the 'store.customer' table
.keyGenerator(automatic("store.customer"));

//Trigger and sequence named 'store.customer_seq'
.keyGenerator(automatic("store.customer_seq"));
Sequence

When sequences are used without triggers the framework can fetch the value from a sequence before insert.

.keyGenerator(sequence("world.city_seq"))
Queried

The framework can select new primary key values from a query.

//Using a query returning the new value
.keyGenerator(queried("""
    select new_id
    from store.id_values
    where table_name = 'store.customer'
    """));
Custom

You can provide a custom key generator strategy by implementing a KeyGenerator.

private static final class UUIDKeyGenerator implements KeyGenerator {

  @Override
  public void beforeInsert(Entity entity, DatabaseConnection connection) {
    entity.put(Customer.ID, UUID.randomUUID().toString());
  }
}
StringFactory

The StringFactory class provides a builder for a Function<Entity, String> instance, which is then used to provide the toString() implementations for entities. This value is used wherever entities are displayed, for example in a ComboBox or as foreign key values in table views.

return Address.TYPE.define(
                Address.ID.define()
                        .primaryKey(),
                Address.STREET.define()
                        .column()
                        .caption("Street")
                        .nullable(false)
                        .maximumLength(120),
                Address.CITY.define()
                        .column()
                        .caption("City")
                        .nullable(false)
                        .maximumLength(50),
                Address.VALID.define()
                        .column()
                        .caption("Valid")
                        .columnHasDefaultValue(true)
                        .defaultValue(true)
                        .nullable(false))
        .stringFactory(StringFactory.builder()
                .value(Address.STREET)
                .text(", ")
                .value(Address.CITY)
                .build())
        .keyGenerator(identity())
        .smallDataset(true)
        .caption("Address")
        .build();

For more complex toString() implementations you can implement a custom Function<Entity, String>.

.stringFactory(new CustomerToString())
private static final class CustomerToString implements Function<Entity, String>, Serializable {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public String apply(Entity customer) {
    StringBuilder builder =
            new StringBuilder(customer.get(Customer.LAST_NAME))
                    .append(", ")
                    .append(customer.get(Customer.FIRST_NAME));
    if (customer.isNotNull(Customer.EMAIL)) {
      builder.append(" <")
              .append(customer.get(Customer.EMAIL))
              .append(">");
    }

    return builder.toString();
  }
}
Validation

Custom validation of Entities is performed by implementing a EntityValidator.

The DefaultEntityValidator implementation provides basic range and null validation and can be extended to provide further validations. Note that validation is performed quite often, so it should not perform expensive operations. Validation requiring database access for example belongs in the application model or ui.

final class CityValidator extends DefaultEntityValidator implements Serializable {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public void validate(Entity city) throws ValidationException {
    super.validate(city);
    //after a call to super.validate() values that are not nullable
    //(such as country and population) are guaranteed to be non-null
    Entity country = city.get(City.COUNTRY_FK);
    Integer cityPopulation = city.get(City.POPULATION);
    Integer countryPopulation = country.get(Country.POPULATION);
    if (countryPopulation != null && cityPopulation > countryPopulation) {
      throw new ValidationException(City.POPULATION,
              cityPopulation, "City population can not exceed country population");
    }
  }
}
.validator(new CityValidator())
Custom data types

When using a custom data type you must specify the columnClass of a ColumnDefinition and provide a Converter implementation.

Column<Location> LOCATION = TYPE.column("location", Location.class);
record Location(double latitude, double longitude) implements Serializable {

  @Override
  public String toString() {
    return "[" + latitude + "," + longitude + "]";
  }
}
Note
The custom type must be serializable for use in an application using the RMI connection.
City.LOCATION.define()
        .column()
        .caption("Location")
        .columnClass(String.class, new LocationConverter())
        .comparator(new LocationComparator()))
private static final class LocationConverter implements Converter<Location, String> {

  @Override
  public String toColumnValue(Location location,
                              Statement statement) {
    return "POINT (" + location.longitude() + " " + location.latitude() + ")";
  }

  @Override
  public Location fromColumnValue(String columnValue) {
    String[] latLon = columnValue
            .replace("POINT (", "")
            .replace(")", "")
            .split(" ");

    return new Location(parseDouble(latLon[1]), parseDouble(latLon[0]));
  }
}

When using the HTTP connection in an application using a custom data type, you must implement a EntityObjectMapperFactory, providing a EntityObjectMapper instance containing a serializer/deserializer for the custom types.

public final class WorldObjectMapperFactory extends DefaultEntityObjectMapperFactory {

  private static final String LATITUDE = "latitude";
  private static final String LONGITUDE = "longitude";

  public WorldObjectMapperFactory() {
    super(World.DOMAIN);
  }

  @Override
  public EntityObjectMapper entityObjectMapper(Entities entities) {
    EntityObjectMapper objectMapper = super.entityObjectMapper(entities);
    objectMapper.addSerializer(Location.class, new LocationSerializer());
    objectMapper.addDeserializer(Location.class, new LocationDeserializer());

    return objectMapper;
  }

  private static final class LocationSerializer extends StdSerializer<Location> {

    private LocationSerializer() {
      super(Location.class);
    }

    @Override
    public void serialize(Location location, JsonGenerator generator, SerializerProvider provider) throws IOException {
      generator.writeStartObject();
      generator.writeNumberField(LATITUDE, location.latitude());
      generator.writeNumberField(LONGITUDE, location.longitude());
      generator.writeEndObject();
    }
  }

  private static final class LocationDeserializer extends StdDeserializer<Location> {

    private LocationDeserializer() {
      super(Location.class);
    }

    @Override
    public Location deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JacksonException {
      JsonNode node = parser.getCodec().readTree(parser);

      return new Location(node.get(LATITUDE).asDouble(), node.get(LONGITUDE).asDouble());
    }
  }
}

This EntityObjectMapperFactory must be exposed to the ServiceLoader.

src/main/java/module-info.java

  provides is.codion.framework.json.domain.EntityObjectMapperFactory
          with is.codion.framework.demos.world.domain.api.WorldObjectMapperFactory;
Entities in action

Using the Entity class is rather straight forward.

EntityConnectionProvider connectionProvider = EntityConnectionProvider.builder()
        .domainType(Petstore.DOMAIN)
        .clientTypeId("Manual")
        .user(User.parse("scott:tiger"))
        .build();

Entities entities = connectionProvider.entities();

EntityConnection connection = connectionProvider.connection();

//populate a new category
Entity insects = entities.builder(Category.TYPE)
        .with(Category.NAME, "Insects")
        .with(Category.DESCRIPTION, "Creepy crawlies")
        .build();

insects = connection.insertSelect(insects);

//populate a new product for the insect category
Entity smallBeetles = entities.builder(Product.TYPE)
        .with(Product.CATEGORY_FK, insects)
        .with(Product.NAME, "Small Beetles")
        .with(Product.DESCRIPTION, "Beetles on the smaller side")
        .build();

connection.insert(smallBeetles);

//see what products are available for the Cats category
Entity categoryCats = connection.selectSingle(Category.NAME.equalTo("Cats"));

List<Entity> cats = connection.select(Product.CATEGORY_FK.equalTo(categoryCats));

cats.forEach(System.out::println);
Unit Testing
Introduction

To unit test the CRUD operations on the domain model extend DomainTest.

The unit tests are run within a single transaction which is rolled back after the test finishes, so these tests are pretty much guaranteed to leave no junk data behind.

DomainTest

The DomainTest uses a default EntityFactory implementation which provides test entities with randomly created values, based on the value constraints set in the domain model. Extend this class and pass to the super constructor, overriding the required methods.

  • foreignKey should return an entity instance for the given foreign key to use for a foreign key reference required for inserting the entity being tested.

  • entity should return an entity to use as basis for the unit test, that is, the entity that should be inserted, selected, updated and finally deleted.

  • modify should simply leave the entity in a modified state so that it can be used for update test, since the database layer throws an exception if an unmodified entity is updated. If modify returns an unmodified entity, the update test is skipped.

To run the full CRUD test for a domain entity you need to call the test(EntityType entityType) method with the entity type as parameter. You can either create a single testDomain() method and call the test method in turn for each entityType or create a entityName method for each domain entity, as we do in the example below.

public class StoreTest extends DomainTest {

  private static final Store DOMAIN = new Store();

  public StoreTest() {
    super(DOMAIN, StoreEntityFactory::new);
  }

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

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

  @Test
  public void customerAddress() throws Exception {
    test(CustomerAddress.TYPE);
  }

  private static final class StoreEntityFactory extends DefaultEntityFactory {

    private StoreEntityFactory(EntityConnection connection) {
      super(connection);
    }

    @Override
    public Optional<Entity> entity(ForeignKey foreignKey) throws DatabaseException {
      //see if the currently running test requires an ADDRESS entity
      if (foreignKey.referencedType().equals(Address.TYPE)) {
        return Optional.of(connection().insertSelect(entities().builder(Address.TYPE)
                .with(Address.ID, 21L)
                .with(Address.STREET, "One Way")
                .with(Address.CITY, "Sin City")
                .build()));
      }

      return super.entity(foreignKey);
    }

    @Override
    public Entity entity(EntityType entityType) throws DatabaseException {
      if (entityType.equals(Address.TYPE)) {
        //Initialize an entity representing the table STORE.ADDRESS,
        //which can be used for the testing
        return entities().builder(Address.TYPE)
                .with(Address.ID, 42L)
                .with(Address.STREET, "Street")
                .with(Address.CITY, "City")
                .with(Address.VALID, true)
                .build();
      }
      else if (entityType.equals(Customer.TYPE)) {
        //Initialize an entity representing the table STORE.CUSTOMER,
        //which can be used for the testing
        return entities().builder(Customer.TYPE)
                .with(Customer.ID, UUID.randomUUID().toString())
                .with(Customer.FIRST_NAME, "Robert")
                .with(Customer.LAST_NAME, "Ford")
                .with(Customer.ACTIVE, true)
                .build();
      }
      else if (entityType.equals(CustomerAddress.TYPE)) {
        return entities().builder(CustomerAddress.TYPE)
                .with(CustomerAddress.CUSTOMER_FK, entity(CustomerAddress.CUSTOMER_FK).orElseThrow())
                .with(CustomerAddress.ADDRESS_FK, entity(CustomerAddress.ADDRESS_FK).orElseThrow())
                .build();
      }

      return super.entity(entityType);
    }

    @Override
    public void modify(Entity entity) {
      if (entity.entityType().equals(Address.TYPE)) {
        entity.put(Address.STREET, "New Street");
        entity.put(Address.CITY, "New City");
      }
      else if (entity.entityType().equals(Customer.TYPE)) {
        //It is sufficient to change the value of a single property, but the more, the merrier
        entity.put(Customer.FIRST_NAME, "Jesse");
        entity.put(Customer.LAST_NAME, "James");
        entity.put(Customer.ACTIVE, false);
      }
    }
  }
}

1.1.2. Conditions

The Chinook domain model is used in the examples below.

Condition
Condition

Represents an element of a where clause and contains factory methods for creating Condition instances.

Condition liveAlbums =
        Album.TITLE.like("Live%");

Entity metallica = selectArtist("Metallica");

Condition metallicaAlbums =
        Album.ARTIST_FK.equalTo(metallica);

Condition allArtistsCondition =
        Condition.all(Artist.TYPE);
Condition.Combination

Extends Condition and represents a combination of Condition instances, which are combined using either AND or OR. These can be nested and combined with other condition combinations.

Condition liveMetallicaAlbumsCondition =
        Condition.and(liveAlbums, metallicaAlbums);
Select
Select

Represents a where clause as well as extended configuration specifically for selecting, such as orderBy, limit, offset and fetchDepth. Select.where returns a Select.Builder instance for further configuration.

Select selectLiveMetallicaAlbums =
        Select.where(liveMetallicaAlbumsCondition)
                .orderBy(OrderBy.descending(Album.NUMBER_OF_TRACKS))
                .build();
Update
Update

Represents a where clause as well as the columns values for updating one or more entities. Update.where returns a Update.Builder instance for specifying the columns to update and their respective values.

Update removeLiveMetallicaAlbumCovers =
        Update.where(liveMetallicaAlbumsCondition)
                .set(Album.COVER, null)
                .build();

1.1.3. EntityConnection

The Codion database layer is extremely thin, it doesn’t perform any joins and provides no access to DBMS specific funtionality except primary key generation via KeyGenerator strategies. The framework provides implementations for the most common strategies, sequences (with or without triggers) and auto increment columns.

The database layer is specified by the EntityConnection class. It provides methods for selecting, inserting, updating and deleting entities, executing procedures and functions, filling reports as well as providing transaction control.

The Chinook domain model is used in the examples below.

Selecting

By default, when you select a row using EntityConnection you receive an Entity instance along with a single level of foreign key references, that is a so-called fetch depth of one. This means that selecting a track you get all the entities referenced via foreign keys as well.

The N+1 problem

This means that selecting tracks performs four queries (track + album, mediatype and genre), but that number of queries is the same whether you select one or a thousand tracks.

The fetch depth can be configured on a foreign key basis when defining entities. A fetch depth of zero means that no foreign key references are fetched, and a value larger than one means that not only is the foreign key reference fetched but also its foreign key references, until the defined depth has been reached. A negative fetch depth means no limit with the whole dependency graph fetched. This limiting of foreign key fetch depth can be turned off, meaning the full reference graph is always fetched, via a system property:

codion.db.limitForeignKeyFetchDepth=false
LocalEntityConnection.LIMIT_FOREIGN_KEY_FETCH_DEPTH.set(false);

or on a connection instance via setLimitForeignKeyFetchDepth()

connection.setLimitForeignKeyFetchDepth(false);
InvoiceLine.INVOICE_FK.define()
        .foreignKey(0)
        .hidden(true),
Track.ALBUM_FK.define()
        .foreignKey(2)
        .attributes(Album.ARTIST_FK, Album.TITLE),
EntityConnection connection = connectionProvider.connection();

List<Entity> tracks = connection.select(
        Track.NAME.like("Bad%"));

Entity track = tracks.get(0);

Entity genre = track.get(Track.GENRE_FK);
Entity mediaType = track.get(Track.MEDIATYPE_FK);
Entity album = track.get(Track.ALBUM_FK);

// fetch depth for Track.ALBUM_FK is 2, which means two levels of
// references are fetched, so we have the artist here as well
Entity artist = album.get(Album.ARTIST_FK);

The fetch depth can also be configured on a query basis, either for the whole query or one or more foreign keys.

EntityConnection connection = connectionProvider.connection();

List<Entity> tracks = connection.select(
        Select.where(Track.NAME.like("Bad%"))
                .fetchDepth(0)
                .build());

Entity track = tracks.get(0);

// fetch depth is 0, so this 'genre' instance is null
Entity genre = track.get(Track.GENRE_FK);

// using track.entity(Track.GENRE_FK) you get a 'genre'
// instance containing only the primary key, since the condition
// fetch depth limit prevented it from being selected
genre = track.entity(Track.GENRE_FK);
EntityConnection connection = connectionProvider.connection();

List<Entity> tracks = connection.select(
        Select.where(Track.NAME.like("Bad%"))
                .fetchDepth(Track.ALBUM_FK, 0)
                .build());

Entity track = tracks.get(0);

Entity genre = track.get(Track.GENRE_FK);
Entity mediaType = track.get(Track.MEDIATYPE_FK);

// this 'album' instance is null, since the condition
// fetch depth limit prevented it from being selected
Entity album = track.get(Track.ALBUM_FK);

// using track.entity(Track.ALBUM_FK) you get a 'album'
// instance containing only the primary key, since the condition
// fetch depth limit prevented it from being selected
album = track.entity(Track.ALBUM_FK);
Selecting entities
EntityConnection connection = connectionProvider.connection();

List<Entity> artists = connection.select(
        Artist.NAME.like("The %"));

List<Entity> nonLiveAlbums = connection.select(and(
        Album.ARTIST_FK.in(artists),
        Album.TITLE.likeIgnoreCase("%live%")));

Entity aliceInChains = connection.selectSingle(
        Artist.NAME.equalTo("Alice In Chains"));

List<Entity> aliceInChainsAlbums = connection.select(
        Album.ARTIST_FK.equalTo(aliceInChains));

Entity metal = connection.selectSingle(
        Genre.NAME.equalToIgnoreCase("metal"));

List<Entity> metalTracks = connection.select(
        Select.where(Track.GENRE_FK.equalTo(metal))
                .attributes(Track.NAME, Track.ALBUM_FK)
                .orderBy(descending(Track.NAME))
                .build());
EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

Entity.Key key = entities.primaryKey(Artist.TYPE, 42L);

Entity artist = connection.select(key);
EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

Entity.Key key42 = entities.primaryKey(Artist.TYPE, 42L);
Entity.Key key43 = entities.primaryKey(Artist.TYPE, 43L);

Collection<Entity> artists = connection.select(asList(key42, key43));
Selecting values

For selecting the values of a single column.

EntityConnection connection = connectionProvider.connection();

List<String> customerUsStates =
        connection.select(Customer.STATE,
                Customer.COUNTRY.equalTo("USA"));
iterator

LocalEntityConnection provides a way to iterate over a result set, instead of loading it into memory.

LocalEntityConnection connection = connectionProvider.connection();

try (ResultIterator<Entity> iterator =
             connection.iterator(Customer.EMAIL.isNotNull())) {
  while (iterator.hasNext()) {
    System.out.println(iterator.next().get(Customer.EMAIL));
  }
}
catch (SQLException e) {
  throw new DatabaseException(e);
}
dependencies

For selecting entities that depend on a set of entities via foreign keys.

EntityConnection connection = connectionProvider.connection();

List<Entity> employees = connection.select(all(Employee.TYPE));

Map<EntityType, Collection<Entity>> dependencies = connection.dependencies(employees);

Collection<Entity> customersDependingOnEmployees = dependencies.get(Customer.TYPE);
count

For selecting the row count given a count condition.

EntityConnection connection = connectionProvider.connection();

int numberOfItStaff = connection.count(where(Employee.TITLE.equalTo("IT Staff")));
Modifying
insert

For inserting rows.

EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

Entity myBand = entities.builder(Artist.TYPE)
        .with(Artist.NAME, "My Band")
        .build();

myBand = connection.insertSelect(myBand);

Entity firstAlbum = entities.builder(Album.TYPE)
        .with(Album.ARTIST_FK, myBand)
        .with(Album.TITLE, "First album")
        .build();
Entity secondAlbum = entities.builder(Album.TYPE)
        .with(Album.ARTIST_FK, myBand)
        .with(Album.TITLE, "Second album")
        .build();

Collection<Entity.Key> albumKeys = connection.insert(asList(firstAlbum, secondAlbum));
update

For updating one or more entity instances. These methods throw an exception if any of the entities is unmodified.

EntityConnection connection = connectionProvider.connection();

Entity myBand = connection.selectSingle(Artist.NAME.equalTo("My Band"));

myBand.put(Artist.NAME, "Proper Name");

myBand = connection.updateSelect(myBand);

List<Entity> customersWithoutPhoneNo =
        connection.select(Customer.PHONE.isNull());

customersWithoutPhoneNo.forEach(customer ->
        customer.put(Customer.PHONE, "<none>"));

connection.update(customersWithoutPhoneNo);
Optimistic locking

The framework performs optimistic locking during updates using the methods above. This is done by selecting the entities being updated FOR UPDATE (when supported by the underlying database) and comparing all original values to the current row values, throwing an exception if one or more values differ or the row is missing.

Note
Excluding attributes when selecting entities results in those attributes (lazy loaded ones for example) not being included when optimistic locking is performed on subsequent updates, since optimistic locking relies on the original attribute value being available for making a comparison.

Optimistic locking can be turned off system-wide using a system property:

codion.db.optimisticLocking=false

or by using the LocalEntityConnection.OPTIMISTIC_LOCKING configuration value:

LocalEntityConnection.OPTIMISTIC_LOCKING.set(false);

or on a connection instance via setOptimisticLocking():

connection.setOptimisticLocking(false);

For updating by condition.

EntityConnection connection = connectionProvider.connection();

connection.update(
        Update.where(Artist.NAME.equalTo("Azymuth"))
                .set(Artist.NAME, "Azymouth")
                .build());

int updateCount = connection.update(
        Update.where(Customer.EMAIL.isNull())
                .set(Customer.EMAIL, "<none>")
                .build());
delete

For deleting existing rows.

EntityConnection connection = connectionProvider.connection();

Entity aquaman = connection.selectSingle(Artist.NAME.equalTo("Aquaman"));

List<Long> aquamanAlbumIds = connection.select(Album.ID,
        Album.ARTIST_FK.equalTo(aquaman));

List<Long> aquamanTrackIds = connection.select(Track.ID,
        Track.ALBUM_ID.in(aquamanAlbumIds));

int playlistTracksDeleted = connection.delete(
        PlaylistTrack.TRACK_ID.in(aquamanTrackIds));

int tracksDeleted = connection.delete(
        Track.ALBUM_ID.in(aquamanAlbumIds));

int albumsDeleted = connection.delete(
        Album.ARTIST_FK.equalTo(aquaman));
EntityConnection connection = connectionProvider.connection();

Entity audioslave = connection.selectSingle(Artist.NAME.equalTo("Audioslave"));

List<Entity> albums = connection.select(Album.ARTIST_FK.equalTo(audioslave));
List<Entity> tracks = connection.select(Track.ALBUM_FK.in(albums));
List<Entity> playlistTracks = connection.select(PlaylistTrack.TRACK_FK.in(tracks));
List<Entity> invoiceLines = connection.select(InvoiceLine.TRACK_FK.in(tracks));

List<Entity.Key> toDelete = new ArrayList<>();
toDelete.addAll(Entity.primaryKeys(invoiceLines));
toDelete.addAll(Entity.primaryKeys(playlistTracks));
toDelete.addAll(Entity.primaryKeys(tracks));
toDelete.addAll(Entity.primaryKeys(albums));
toDelete.add(audioslave.primaryKey());

connection.delete(toDelete);
Procedures & Functions
Function
EntityConnection connection = connectionProvider.connection();

List<Long> trackIds = asList(123L, 1234L);
BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

Collection<Entity> modifiedTracks =
        connection.execute(Track.RAISE_PRICE,
                new RaisePriceParameters(trackIds, priceIncrease));

Collection<Entity> updatedInvoices =
        connection.execute(Invoice.UPDATE_TOTALS, Arrays.asList(1234L, 3412L));

String playlistName = "Random playlist";
int numberOfTracks = 100;
Collection<Entity> playlistGenres = connection.select(
        Genre.NAME.in("Classical", "Soundtrack"));

Entity playlist = connection.execute(Playlist.RANDOM_PLAYLIST,
        new RandomPlaylistParameters(playlistName, numberOfTracks, playlistGenres));
Procedure

Executing procedures is the same as executing functions, except without a return value.

Reporting
report
EntityConnection connection = connectionProvider.connection();

Map<String, Object> reportParameters = new HashMap<>();
reportParameters.put("CUSTOMER_IDS", asList(42, 43, 45));

JasperPrint jasperPrint = connection.report(Customer.REPORT, reportParameters);
Transaction control
Transactional

The recommended way to use transactions is to use either of the following, depending on whether a return value is required.

These methods perform a commit on success and rollback on failure.

Transaction without a result
EntityConnection connection = connectionProvider.connection();

EntityConnection.transaction(connection, () -> {
  Entities entities = connection.entities();

  Entity artist = entities.builder(Artist.TYPE)
          .with(Artist.NAME, "The Band")
          .build();
  artist = connection.insertSelect(artist);

  Entity album = entities.builder(Album.TYPE)
          .with(Album.ARTIST_FK, artist)
          .with(Album.TITLE, "The Album")
          .build();

  connection.insert(album);
});
Same example using an anonymous class
EntityConnection connection = connectionProvider.connection();

Transactional transactional = new Transactional() {

  @Override
  public void execute() throws DatabaseException {
    Entities entities = connection.entities();

    Entity artist = entities.builder(Artist.TYPE)
            .with(Artist.NAME, "The Band")
            .build();
    artist = connection.insertSelect(artist);

    Entity album = entities.builder(Album.TYPE)
            .with(Album.ARTIST_FK, artist)
            .with(Album.TITLE, "The Album")
            .build();

    connection.insert(album);
  }
};

EntityConnection.transaction(connection, transactional);
Transaction with a result
EntityConnection connection = connectionProvider.connection();

Entity.Key albumKey = EntityConnection.transaction(connection, () -> {
  Entities entities = connection.entities();

  Entity artist = entities.builder(Artist.TYPE)
          .with(Artist.NAME, "The Band")
          .build();
  artist = connection.insertSelect(artist);

  Entity album = entities.builder(Album.TYPE)
          .with(Album.ARTIST_FK, artist)
          .with(Album.TITLE, "The Album")
          .build();

  return connection.insert(album);
});
Same example using an anonymous class
EntityConnection connection = connectionProvider.connection();

TransactionalResult<Entity.Key> transactional = new TransactionalResult<Entity.Key>() {

  @Override
  public Entity.Key execute() throws DatabaseException {
    Entities entities = connection.entities();

    Entity artist = entities.builder(Artist.TYPE)
            .with(Artist.NAME, "The Band")
            .build();
    artist = connection.insertSelect(artist);

    Entity album = entities.builder(Album.TYPE)
            .with(Album.ARTIST_FK, artist)
            .with(Album.TITLE, "The Album")
            .build();

    return connection.insert(album);
  }
};

Entity.Key albumKey = EntityConnection.transaction(connection, transactional);
Transaction

For a more fine grained transaction control and the ability to rollback, transactions can be started and ended manually, note that this is more complex and thereby error prone and should not be used unless the method described above does not work for your use-case.

EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

// It is very important to start the transaction here, outside of the try/catch block,
// otherwise, trying to start a transaction on a connection already with an open transaction
// (which is a bug in itself), would cause the current transaction to be rolled back
// in the Exception catch block, which is probably not what you want.
connection.startTransaction();
try {
  Entity artist = entities.builder(Artist.TYPE)
          .with(Artist.NAME, "The Band")
          .build();
  connection.insert(artist);

  Entity album = entities.builder(Album.TYPE)
          .with(Album.ARTIST_FK, artist)
          .with(Album.TITLE, "The Album")
          .build();
  connection.insert(album);

  connection.commitTransaction();
}
catch (DatabaseException e) {
  connection.rollbackTransaction();
  throw e;
}
catch (RuntimeException e) {
  // It is a good practice, but not necessary, to catch RuntimeException,
  // in order to not wrap a RuntimeException in another RuntimeException.
  connection.rollbackTransaction();
  throw e;
}
catch (Exception e) {
  // Always include a catch for the top level Exception, otherwise unexpected
  // exceptions may cause a transaction to remain open, which is a very serious bug.
  connection.rollbackTransaction();
  throw new RuntimeException(e);
}
LocalEntityConnection

A EntityConnection implementation based on a direct connection to the database, provides access to the underlying JDBC connection.

RemoteEntityConnection

A EntityConnection implementation based on an RMI connection. Requires a server.

HttpEntityConnection

A EntityConnection implementation based on HTTP. Requires a server.

1.1.4. EntityConnectionProvider

In most cases EntityConnections are retrieved from a EntityConnectionProvider, which is responsible for establishing a connection to the underlying database. The EntityConnectionProvider class is central to the framework and is a common constructor parameter in classes requiring database access.

The EntityConnectionProvider manages a single connection, that is, the one returned by connection(). If a connection becomes invalid, i.e. due to a network outage or a server restart the EntityConnectionProvider is responsible for reconnecting and returning a new valid connection. If the EntityConnectionProvider is unable to connect to the underlying database or server connection() throws an exception.

A reference to the EntityConnection instance returned by connection() should only be kept for a short time, i.e. as a method field or parameter, and should not be cached or kept as a class field since it can become invalid and thereby unusable. Always use connection() to be sure you have a healthy EntityConnection.

LocalEntityConnectionProvider

Provides a connection based on a local JDBC connection.

Database.DATABASE_URL.set("jdbc:h2:mem:h2db");
Database.DATABASE_INIT_SCRIPTS.set("src/main/sql/create_schema.sql");

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

LocalEntityConnection entityConnection =
        connectionProvider.connection();

DatabaseConnection databaseConnection =
        entityConnection.databaseConnection();

// the underlying JDBC connection is available in a local connection
Connection connection = databaseConnection.getConnection();

connectionProvider.close();
RemoteEntityConnectionProvider

Provides a connection based on a remote RMI connection.

RemoteEntityConnectionProvider connectionProvider =
        RemoteEntityConnectionProvider.builder()
                .domainType(Chinook.DOMAIN)
                .user(User.parse("scott:tiger"))
                .clientTypeId(EntityConnectionProviderDemo.class.getSimpleName())
                .hostName("localhost")
                .registryPort(1099)
                .build();

EntityConnection entityConnection =
        connectionProvider.connection();

Entities entities = entityConnection.entities();

Entity track = entityConnection.select(entities.primaryKey(Track.TYPE, 42L));

connectionProvider.close();
HttpEntityConnectionProvider

Provides a connection based on a remote HTTP connection.

HttpEntityConnectionProvider connectionProvider =
        HttpEntityConnectionProvider.builder()
                .domainType(Chinook.DOMAIN)
                .clientTypeId(EntityConnectionProviderDemo.class.getSimpleName())
                .user(User.parse("scott:tiger"))
                .hostName("localhost")
                .port(8080)
                .https(false)
                .build();

EntityConnection entityConnection = connectionProvider.connection();

Entities entities = entityConnection.entities();

entityConnection.select(entities.primaryKey(Track.TYPE, 42L));

connectionProvider.close();

1.2. Framework Model

1.2.1. EntityModel

The application model layer consists of the EntityModel class and its associates; the EntityTableModel, which provides a table representation of entities and the EntityEditModel which provides the CRUD operations.

An EntityModel always contains an EntityEditModel instance and usually contains a EntityTableModel as well.

entity model diagram
public class AddressModel extends SwingEntityModel {

  public AddressModel(EntityConnectionProvider connectionProvider) {
    super(Address.TYPE, connectionProvider);
  }
}
public class CustomerModel extends SwingEntityModel {

  public CustomerModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerTableModel(connectionProvider));
    bindEvents();
  }

  private void bindEvents() {
    tableModel().refresher().observer().addConsumer(refreshing -> {
      if (refreshing) {
        System.out.println("Refresh is about to start");
      }
      else {
        System.out.println("Refresh is about to end");
      }
    });

    editModel().value(Customer.FIRST_NAME).addConsumer(value ->
            System.out.println("First name changed to " + value));
  }
}
public class CustomerAddressModel extends SwingEntityModel {

  public CustomerAddressModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerAddressTableModel(connectionProvider));
  }
}
EntityTableModel

Every EntityTableModel contains a EntityEditModel instance. A default edit model implementation is created automatically by the EntityTableModel if one is not supplied via a constructor argument.

public class CustomerTableModel extends SwingEntityTableModel {

  public CustomerTableModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerEditModel(connectionProvider));
  }
}
EntityEditModel
public class CustomerEditModel extends SwingEntityEditModel {

  public CustomerEditModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
Detail models

An EntityModel can contain one or more detail models, usually based on foreign key relationships.

entity detail model diagram
public class StoreApplicationModel extends SwingEntityApplicationModel {

  public StoreApplicationModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider);

    CustomerModel customerModel = new CustomerModel(connectionProvider);
    CustomerAddressModel customerAddressModel = new CustomerAddressModel(connectionProvider);

    customerModel.addDetailModel(customerAddressModel);

    //populate the model with rows from the database
    customerModel.tableModel().refresh();

    addEntityModel(customerModel);
  }
}
Event binding

The model layer classes expose a number of Event, State and Value observers.

The example below prints, to the standard output, all changes made to a given attribute value as well as a message indicating that a table refresh has started.

public class CustomerModel extends SwingEntityModel {

  public CustomerModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerTableModel(connectionProvider));
    bindEvents();
  }

  private void bindEvents() {
    tableModel().refresher().observer().addConsumer(refreshing -> {
      if (refreshing) {
        System.out.println("Refresh is about to start");
      }
      else {
        System.out.println("Refresh is about to end");
      }
    });

    editModel().value(Customer.FIRST_NAME).addConsumer(value ->
            System.out.println("First name changed to " + value));
  }
}

1.2.2. EntityEditModel

entity edit model diagram

The EntityEditModel interface defines the CRUD business logic used by the EntityEditPanel class when entities are being edited. The EntityEditModel works with a single entity instance, which can be retrieved and set via the Mutable<Entity> instance accessible via the entity() method. The EntityEditModel interface exposes a number of methods for manipulating as well as querying the values of the active entity.

public class CustomerEditModel extends SwingEntityEditModel {

  public CustomerEditModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
EntityConnectionProvider connectionProvider =
        EntityConnectionProvider.builder()
                .domainType(Store.DOMAIN)
                .user(User.parse("scott:tiger"))
                .clientTypeId("StoreMisc")
                .build();

CustomerEditModel editModel = new CustomerEditModel(connectionProvider);

editModel.value(Customer.ID).set(UUID.randomUUID().toString());
editModel.value(Customer.FIRST_NAME).set("Björn");
editModel.value(Customer.LAST_NAME).set("Sigurðsson");
editModel.value(Customer.ACTIVE).set(true);

//inserts and returns the inserted entity
Entity customer = editModel.insert();

//modify some values
editModel.value(Customer.FIRST_NAME).set("John");
editModel.value(Customer.LAST_NAME).set("Doe");

//updates and returns the updated entity
customer = editModel.update();

//deletes the active entity
editModel.delete();

1.2.3. EntityTableModel

entity table model diagram

The EntityTableModel class provides a table representation of the underlying entities.

public class CustomerTableModel extends SwingEntityTableModel {

  public CustomerTableModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerEditModel(connectionProvider));
  }
}
public class CustomerAddressTableModel extends SwingEntityTableModel {

  public CustomerAddressTableModel(EntityConnectionProvider connectionProvider) {
    super(CustomerAddress.TYPE, connectionProvider);
  }
}

1.2.4. EntityApplicationModel

entity application model diagram

The EntityApplicationModel class serves as the base for the application. Its main purpose is to hold references to the root EntityModel instances used by the application.

When extending this class you must provide a constructor with a single EntityConnectionProvider parameter, as seen below.

public class StoreApplicationModel extends SwingEntityApplicationModel {

  public StoreApplicationModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider);

    CustomerModel customerModel = new CustomerModel(connectionProvider);
    CustomerAddressModel customerAddressModel = new CustomerAddressModel(connectionProvider);

    customerModel.addDetailModel(customerAddressModel);

    //populate the model with rows from the database
    customerModel.tableModel().refresh();

    addEntityModel(customerModel);
  }
}

1.2.5. Application load testing

The application load testing harness is used to see how your application, server and database handle multiple concurrent users.

This is done by using the LoadTestModel and LoadTestPanel classes as shown below.

public class StoreLoadTest {

  private static final class StoreApplicationModelFactory
          implements Function<User, StoreApplicationModel> {

    @Override
    public StoreApplicationModel apply(User user) {
      EntityConnectionProvider connectionProvider =
              RemoteEntityConnectionProvider.builder()
                      .user(user)
                      .domainType(Store.DOMAIN)
                      .build();

      return new StoreApplicationModel(connectionProvider);
    }
  }

  private static class StoreScenarioPerformer
          implements Performer<StoreApplicationModel> {

    private static final Random RANDOM = new Random();

    @Override
    public void perform(StoreApplicationModel application) {
      SwingEntityModel customerModel = application.entityModel(Customer.TYPE);
      customerModel.tableModel().refresh();
      selectRandomRow(customerModel.tableModel());
    }

    private static void selectRandomRow(EntityTableModel<?> tableModel) {
      if (tableModel.items().visible().count() > 0) {
        tableModel.selection().index().set(RANDOM.nextInt(tableModel.items().visible().count()));
      }
    }
  }

  public static void main(String[] args) {
    LoadTest<StoreApplicationModel> loadTest =
            LoadTest.builder(new StoreApplicationModelFactory(),
                            application -> application.connectionProvider().close())
                    .user(User.parse("scott:tiger"))
                    .scenarios(List.of(scenario(new StoreScenarioPerformer())))
                    .name("Store LoadTest - " + EntityConnectionProvider.CLIENT_CONNECTION_TYPE.get())
                    .build();
    loadTestPanel(loadTestModel(loadTest)).run();
  }
}

1.3. Framework UI

1.3.1. EntityPanel

entity panel diagram

The EntityPanel is the base UI class for working with entity instances. It usually consists of an EntityTablePanel, an EntityEditPanel, and a set of detail panels representing the entities having a master/detail relationship with the underlying entity.

Basics

You can either extend the EntityPanel class or instantiate one directly, depending on your needs.

public class AddressPanel extends EntityPanel {

  public AddressPanel(SwingEntityModel addressModel) {
    super(addressModel, new AddressEditPanel(addressModel.editModel()));
  }
}
SwingEntityModel addressModel =
        new SwingEntityModel(Address.TYPE, connectionProvider);

EntityPanel addressPanel =
        new EntityPanel(addressModel,
                new AddressEditPanel(addressModel.editModel()));
Detail panels

Adding a detail panel is done with a single method call, but note that the underlying EntityModel must contain the correct detail model for the detail panel, in this case a CustomerModel instance, see detail models. See EntityApplicationPanel.

public class CustomerPanel extends EntityPanel {

  public CustomerPanel(SwingEntityModel entityModel) {
    super(entityModel);
    SwingEntityModel addressModel =
            entityModel.detailModel(Address.TYPE);
    addDetailPanel(new AddressPanel(addressModel));
  }
}

1.3.2. EntityEditPanel

The EntityEditPanel manages the input components (text fields, combo boxes and such) for editing an entity instance.

When extending an EntityEditPanel you must implement the initializeUI() method, which initializes the edit panel UI. The EntityEditPanel class exposes methods for creating input components and binding them with the underlying EntityEditModel instance.

public class CustomerEditPanel extends EntityEditPanel {

  public CustomerEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
    defaults().textFieldColumns().set(15);
  }

  @Override
  protected void initializeUI() {
    //the firstName field should receive the focus whenever the panel is initialized
    initialFocusAttribute().set(Customer.FIRST_NAME);

    createTextField(Customer.FIRST_NAME);
    createTextField(Customer.LAST_NAME);
    createTextField(Customer.EMAIL);
    createCheckBox(Customer.ACTIVE);

    setLayout(new GridLayout(4, 1));
    //the addInputPanel method creates and adds a panel containing the
    //component associated with the attribute as well as a JLabel with the
    //property caption as defined in the domain model
    addInputPanel(Customer.FIRST_NAME);
    addInputPanel(Customer.LAST_NAME);
    addInputPanel(Customer.EMAIL);
    addInputPanel(Customer.ACTIVE);
  }
}
public class AddressEditPanel extends EntityEditPanel {

  public AddressEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Address.STREET);

    createTextField(Address.STREET).columns(25);
    createTextField(Address.CITY).columns(25);
    createCheckBox(Address.VALID);

    setLayout(new GridLayout(3, 1, 5, 5));
    addInputPanel(Address.STREET);
    addInputPanel(Address.CITY);
    addInputPanel(Address.VALID);
  }
}
public class CustomerAddressEditPanel extends EntityEditPanel {

  public CustomerAddressEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(CustomerAddress.ADDRESS_FK);

    createForeignKeyComboBoxPanel(CustomerAddress.ADDRESS_FK, this::createAddressEditPanel)
            .preferredWidth(280)
            .includeAddButton(true);

    setLayout(borderLayout());

    addInputPanel(CustomerAddress.ADDRESS_FK);
  }

  private AddressEditPanel createAddressEditPanel() {
    return new AddressEditPanel(new SwingEntityEditModel(Address.TYPE, editModel().connectionProvider()));
  }
}
Detailed example

Here’s how a text field is created and added to the edit panel.

createTextField(Customer.FIRST_NAME)
        .columns(12);

setLayout(gridLayout(1, 1));
addInputPanel(Customer.FIRST_NAME);

And here’s the equivilent code, showing what’s going on behind the scenes.

ColumnDefinition<String> firstNameDefinition =
        editModel().entityDefinition().columns().definition(Customer.FIRST_NAME);

//create the text field
JTextField firstNameField = new JTextField();
firstNameField.setColumns(12);
firstNameField.setToolTipText(firstNameDefinition.description());
//associate the text field with the first name attribute
component(Customer.FIRST_NAME).set(firstNameField);

//wrap the text field in a ComponentValue
ComponentValue<String, JTextField> firstNameFieldValue =
        new AbstractTextComponentValue<String, JTextField>(firstNameField) {
          @Override
          protected String getComponentValue() {
            return component().getText();
          }

          @Override
          protected void setComponentValue(String text) {
            component().setText(text);
          }
        };

//link the component value to the attribute value in the edit model
firstNameFieldValue.link(editModel().value(Customer.FIRST_NAME));

//create the first name label
JLabel firstNameLabel = new JLabel(firstNameDefinition.caption());
//associate the label with the text field
firstNameLabel.setLabelFor(firstNameField);

//create an input panel, with the label and text field
JPanel firstNamePanel = new JPanel(borderLayout());
firstNamePanel.add(firstNameLabel, BorderLayout.NORTH);
firstNamePanel.add(firstNameField, BorderLayout.CENTER);

setLayout(gridLayout(1, 1));
add(firstNamePanel);
Input controls
Boolean
JCheckBox checkBox =
        createCheckBox(Demo.BOOLEAN)
                .build();

NullableCheckBox nullableCheckBox =
        (NullableCheckBox) createCheckBox(Demo.BOOLEAN)
                .nullable(true)
                .build();

JComboBox<Item<Boolean>> comboBox =
        createBooleanComboBox(Demo.BOOLEAN)
                .build();
Foreign key
EntityComboBox comboBox =
        createForeignKeyComboBox(Demo.FOREIGN_KEY)
                .build();

EntitySearchField searchField =
        createForeignKeySearchField(Demo.FOREIGN_KEY)
                .build();

//readOnly
JLabel label =
        createForeignKeyLabel(Demo.FOREIGN_KEY)
                .build();
Temporal
TemporalField<LocalDateTime> textField =
        (TemporalField<LocalDateTime>) createTextField(Demo.LOCAL_DATE)
                .build();

TemporalField<LocalDate> localDateField =
        createTemporalField(Demo.LOCAL_DATE)
                .build();

TemporalFieldPanel<LocalDate> temporalPanel =
        createTemporalFieldPanel(Demo.LOCAL_DATE)
                .build();
Numerical
NumberField<Integer> integerField =
        (NumberField<Integer>) createTextField(Demo.INTEGER)
                .build();

integerField =
        createIntegerField(Demo.INTEGER)
                .build();

NumberField<Long> longField =
        (NumberField<Long>) createTextField(Demo.LONG)
                .build();

longField =
        createLongField(Demo.LONG)
                .build();

NumberField<Double> doubleField =
        (NumberField<Double>) createTextField(Demo.DOUBLE)
                .build();

doubleField =
        createDoubleField(Demo.DOUBLE)
                .build();

NumberField<BigDecimal> bigDecimalField =
        (NumberField<BigDecimal>) createTextField(Demo.BIG_DECIMAL)
                .build();

bigDecimalField =
        createBigDecimalField(Demo.BIG_DECIMAL)
                .build();
Text
JTextField textField =
        createTextField(Demo.TEXT)
                .build();

JFormattedTextField maskedField =
        createMaskedTextField(Demo.FORMATTED_TEXT)
                .mask("###:###")
                .valueContainsLiteralCharacters(true)
                .build();

JTextArea textArea =
        createTextArea(Demo.LONG_TEXT)
                .rowsColumns(5, 20)
                .build();

TextFieldPanel inputPanel =
        createTextFieldPanel(Demo.LONG_TEXT)
                .build();
Selection
DefaultComboBoxModel<String> comboBoxModel =
        new DefaultComboBoxModel<>(new String[] {"One", "Two"});

JComboBox<String> comboBox =
        createComboBox(Demo.TEXT, comboBoxModel)
                .editable(true)
                .build();
Items
JComboBox<Item<String>> comboBox =
        createItemComboBox(Demo.ITEM_LIST)
                .build();
Panels & labels
JLabel label = createLabel(Demo.TEXT)
        .build();

JPanel inputPanel = createInputPanel(Demo.TEXT);
Custom actions

The action mechanism used throughout the Codion framework is based on the Control class and its subclasses and the Controls class which represents a collection of controls.

1.3.3. EntityTablePanel

The EntityTablePanel provides a table view of entities.

Adding a print action

The most common place to add a custom control is the table popup menu, i.e. an action for printing reports or for acting on the selected rows. For the simplest case, where a single print action is required, a custom control can be associated with the PRINT ControlKey, this control will appear in the Print submenu in the table popup menu as well as on the table toolbar.

For more complex cases, where multiple print controls are required, custom controls can be associated with the PRINT_CONTROLS ControlKey.

public class CustomerTablePanel extends EntityTablePanel {

  public CustomerTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
    // associate a custom Control with the PRINT control key,
    // which calls the viewCustomerReport method in this class,
    // enabled only when the selection is not empty
    control(PRINT).set(Control.builder()
            .command(this::viewCustomerReport)
            .name("Customer report")
            .smallIcon(FrameworkIcons.instance().print())
            .enabled(tableModel().selection().empty().not())
            .build());
  }

  private void viewCustomerReport() throws Exception {
    List<Entity> selectedCustomers = tableModel().selection().items().get();
    if (selectedCustomers.isEmpty()) {
      return;
    }

    Collection<String> customerIds = Entity.values(Customer.ID, selectedCustomers);
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", customerIds);

    JasperPrint customerReport = tableModel().connection()
            .report(Customer.REPORT, reportParameters);

    Dialogs.componentDialog(new JRViewer(customerReport))
            .owner(this)
            .modal(false)
            .title("Customer Report")
            .size(new Dimension(800, 600))
            .show();
  }
}

1.3.4. EntityPanel.Builder

Use the EntityPanel.Builder class to specify a EntityPanel class configuration, for panels that should not be initialized until used, such the support table panels.

  @Override
  protected List<EntityPanel.Builder> createSupportEntityPanelBuilders() {
    EntityPanel.Builder addressPanelBuilder =
            EntityPanel.builder(Address.TYPE)
                    .editPanel(AddressEditPanel.class);

    return List.of(addressPanelBuilder);
  }

1.3.5. EntityApplicationPanel

The EntityApplicationPanel class serves as the main application UI. When extending this class you must provide a constructor with a single application model parameter, as seen below.

public class StoreApplicationPanel extends EntityApplicationPanel<StoreApplicationModel> {

  public StoreApplicationPanel(StoreApplicationModel applicationModel) {
    super(applicationModel);
  }

  @Override
  protected List<EntityPanel> createEntityPanels() {
    CustomerModel customerModel =
            applicationModel().entityModel(Customer.TYPE);
    CustomerAddressModel customerAddressModel =
            customerModel.detailModel(CustomerAddress.TYPE);

    EntityPanel customerPanel = new EntityPanel(customerModel,
            new CustomerEditPanel(customerModel.editModel()),
            new CustomerTablePanel(customerModel.tableModel()));
    EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
            new CustomerAddressEditPanel(customerAddressModel.editModel()));

    customerPanel.addDetailPanel(customerAddressPanel);

    return List.of(customerPanel);
  }

  @Override
  protected List<EntityPanel.Builder> createSupportEntityPanelBuilders() {
    EntityPanel.Builder addressPanelBuilder =
            EntityPanel.builder(Address.TYPE)
                    .editPanel(AddressEditPanel.class);

    return List.of(addressPanelBuilder);
  }

  public static void main(String[] args) {
    Locale.setDefault(new Locale("en", "EN"));
    EntityPanel.Config.TOOLBAR_CONTROLS.set(true);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING
            .set(ReferentialIntegrityErrorHandling.DISPLAY_DEPENDENCIES);
    EntityApplicationPanel.builder(StoreApplicationModel.class, StoreApplicationPanel.class)
            .applicationName("Store")
            .domainType(Store.DOMAIN)
            .defaultLoginUser(User.parse("scott:tiger"))
            .start();
  }
}

1.3.6. Reporting with JasperReports

Codion uses a plugin oriented approach to report viewing and provides an implementation for JasperReports.

With the Codion JasperReports plugin you can either design your report based on an SQL query in which case you use the JRReport class, which facilitates the report being filled using the active database connection, or you can design your report around the JRDataSource implementation provided by the JasperReportsDataSource class, which is constructed around an iterator.

JDBC Reports

Using a report based on an SQL query is the simplest way of viewing a report using Codion, just add a method similar to the one below to a EntityTablePanel subclass. You can then create an action calling that method and put it in for example the table popup menu as described in the adding a print action section.

public class CustomerTablePanel extends EntityTablePanel {

  public CustomerTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
    // associate a custom Control with the PRINT control key,
    // which calls the viewCustomerReport method in this class,
    // enabled only when the selection is not empty
    control(PRINT).set(Control.builder()
            .command(this::viewCustomerReport)
            .name("Customer report")
            .smallIcon(FrameworkIcons.instance().print())
            .enabled(tableModel().selection().empty().not())
            .build());
  }

  private void viewCustomerReport() throws Exception {
    List<Entity> selectedCustomers = tableModel().selection().items().get();
    if (selectedCustomers.isEmpty()) {
      return;
    }

    Collection<String> customerIds = Entity.values(Customer.ID, selectedCustomers);
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", customerIds);

    JasperPrint customerReport = tableModel().connection()
            .report(Customer.REPORT, reportParameters);

    Dialogs.componentDialog(new JRViewer(customerReport))
            .owner(this)
            .modal(false)
            .title("Customer Report")
            .size(new Dimension(800, 600))
            .show();
  }
}
JRDataSource Reports

The JRDataSource implementation provided by the JasperReportsDataSource simply iterates through the iterator received via the constructor and retrieves the field values from the underlying entities. The easiest way to make this work is to design the report using field names that correspond to the attribute names, so using the Store domain example from above the fields in a report showing the available items would have to be named 'name', 'active', 'category_code' etc.

EntityConnection connection = connectionProvider.connection();

EntityDefinition customerDefinition =
        connection.entities().definition(Customer.TYPE);

Iterator<Entity> customerIterator =
        connection.select(all(Customer.TYPE)).iterator();

JasperReportsDataSource<Entity> dataSource =
        new JasperReportsDataSource<>(customerIterator,
                (entity, reportField) ->
                        entity.get(customerDefinition.attributes().get(reportField.getName())));

JRReport customerReport = fileReport("reports/customer.jasper");

JasperPrint jasperPrint = JasperReports.fillReport(customerReport, dataSource);

2. Common

2.1. Common Core

2.1.1. Core classes

Three common classes used throughout the framework are Event, State and Value and their respective observers Observer, StateObserver and ValueObserver.

Event
event diagram

The Event class is a synchronous event implementation used throughout the framework. Classes typically expose observers for their events via public accessors. Events are triggered by calling the run method in case no data is associated with the event or accept in case data should be propogated to listeners.

The associated Observer instance can not trigger the event and can be safely passed around.

Event listeners must implement either Runnable or Consumer, depending on whether they use the data associated with the event.

Events are instantiated via factory methods in the Event class.

// specify an event propagating
// a String as the event data
Event<String> event = Event.event();

// an observer manages the listeners
// for an Event but can not trigger it
Observer<String> observer = event.observer();

// add a listener if you're not
// interested in the event data
observer.addListener(() -> System.out.println("Event occurred"));

event.run();//output: 'Event occurred'

// or a consumer if you're
// interested in the event data
observer.addConsumer(data -> System.out.println("Event: " + data));

event.accept("info");//output: 'Event: info'

// Event implements Observer so
// listeneres can be added directly without
// referring to the Observer
event.addConsumer(System.out::println);
Value
value diagram

A Value wraps a value and provides a change listener.

Values are instantiated via factory methods in the Value class.

Values can be linked so that changes in one are reflected in the other.

// a nullable value with 2 as the initial value
Value<Integer> value =
        Value.value(2);

value.set(4);

// a non-null value using 0 as null replacement
Value<Integer> otherValue =
        Value.builder()
                .nonNull(0)
                // linked to the value above
                .link(value)
                .build();

System.out.println(otherValue.get());// output: 4

otherValue.set(3);

System.out.println(value.get());// output: 3

value.addConsumer(System.out::println);

otherValue.addListener(() ->
        System.out.println("Value changed: " + otherValue.get()));

Values can be non-nullable if a nullValue is specified when the value is initialized. Null is then translated to the nullValue when set.

Integer initialValue = 42;
Integer nullValue = 0;

Value<Integer> value =
        Value.builder()
                .nonNull(nullValue)
                .value(initialValue)
                .build();

System.out.println(value.nullable());//output: false

System.out.println(value.get());// output: 42

value.set(null); //or value.clear();

System.out.println(value.get());//output: 0
State
state diagram

The State class encapsulates a boolean state and provides read only access and a change observer via StateObserver. A State implements Value<Boolean> and is non-nullable with null translating to false.

States are instantiated via factory methods in the State class.

// a boolean state, false by default
State state = State.state();

// an observer manages the listeners for a State but can not modify it
StateObserver observer = state.observer();
// a not observer is always available, which is
// always the reverse of the original state
StateObserver not = state.not();

// add a listener notified each time the state changes
observer.addListener(() -> System.out.println("State changed"));

state.set(true);//output: 'State changed'

observer.addConsumer(value -> System.out.println("State: " + value));

state.set(null);//output: 'State: false'

// State extends StateObserver so listeners can be added
// directly without referring to the StateObserver
state.addListener(() -> System.out.println("State changed"));
private static final class IntegerValue {

  private final State negative = State.state(false);
  private final Value<Integer> integer = Value.builder()
          .nonNull(0)
          .consumer(value -> negative.set(value < 0))
          .build();

  /**
   * Increment the value by one
   */
  public void increment() {
    integer.map(value -> value + 1);
  }

  /**
   * Decrement the value by one
   */
  public void decrement() {
    integer.map(value -> value - 1);
  }

  /**
   * @return an observer notified each time the value changes
   */
  public Observer<Integer> changed() {
    return integer.observer();
  }

  /**
   * @return a state observer indicating whether the value is negative
   */
  public StateObserver negative() {
    return negative.observer();
  }
}

Any Action object can be linked to a State instance via the Utilities.linkToEnabledState method, where the action’s enabled status is updated according to the state.

State state = State.state();

Action action = new AbstractAction("action") {
  @Override
  public void actionPerformed(ActionEvent e) {
    System.out.println("Hello Action");
  }
};

Utilities.linkToEnabledState(state, action);

System.out.println(action.isEnabled());// output: false

state.set(true);

System.out.println(action.isEnabled());// output: true

Controls can also be linked to a State instance.

State state = State.state();

CommandControl control = Control.builder()
        .command(() -> System.out.println("Hello Control"))
        .enabled(state)
        .build();

System.out.println(control.isEnabled());// output: false

state.set(true);

System.out.println(control.isEnabled());// output: true
Note
When a State or Event is linked to a Swing component, for example its enabled state, all state changes must happen on the Event Dispatch Thread.

2.2. Common Database

The core JDBC related classes.

2.2.1. Common classes

Two common classes used throughout the framework are the Database and DatabaseConnection classes.

Database

The Database class represents a DBMS instance and provides connections to that instance.

There are multiple ways to aquire a Database instance.

  • By specifying a JDBC url via a system property.

System.setProperty("codion.db.url", "jdbc:h2:mem:h2db");

Database database = Database.instance();
  • By setting the JDBC url configuration value directly (which also sets the system property).

Database.DATABASE_URL.set("jdbc:h2:mem:h2db");

Database database = Database.instance();
  • By instantiating a DatabaseFactory directly.

String url = "jdbc:h2:mem:h2db";

DatabaseFactory databaseFactory = DatabaseFactory.instance(url);

Database database = databaseFactory.create(url);
  • By instantiating a DBMS specific DatabaseFactory directly.

String url = "jdbc:h2:mem:h2db";

H2DatabaseFactory databaseFactory = new H2DatabaseFactory();

Database database = databaseFactory.create(url);

A Database instance provides java.sql.Connection instances via the createConnection method.

Database.DATABASE_URL.set("jdbc:h2:mem:h2db");

Database database = Database.instance();

User user = User.parse("scott:tiger");

java.sql.Connection connection = database.createConnection(user);
DatabaseConnection

The DatabaseConnection class represents a connection to a database instance and is a wrapper around a java.sql.Connection instance and provides transaction control.

A DatabaseConnection instance is created via the databaseConnection factory methods.

Database.DATABASE_URL.set("jdbc:h2:mem:h2db");

Database database = Database.instance();

User user = User.parse("scott:tiger");

DatabaseConnection databaseConnection =
        DatabaseConnection.databaseConnection(database, user);

databaseConnection.startTransaction();
try {
  java.sql.Connection connection = databaseConnection.getConnection();
  connection.createStatement().execute("select 1");
  databaseConnection.commitTransaction();
}
catch (SQLException e) {
  databaseConnection.rollbackTransaction();
  throw new DatabaseException(e);
}
catch (Exception e) {
  databaseConnection.rollbackTransaction();
  throw new RuntimeException(e);
}

databaseConnection.close();

2.3. Common Model

2.3.1. Table Model

FilterTableModel
filter table model diagram

The FilterTableModel is a table model central to the framework.

// Define a record representing the table rows
public record Person(String name, int age) {}

// Define an enum identifying the table columns
public enum PersonColumn {
  NAME,
  AGE
}

// Implement Columns, specifying the table columns
public static final class PersonColumns implements Columns<Person, PersonColumn> {

  private static final List<PersonColumn> COLUMNS = List.of(PersonColumn.values());

  @Override
  public List<PersonColumn> identifiers() {
    return COLUMNS;
  }

  @Override
  public Class<?> columnClass(PersonColumn column) {
    return switch (column) {
      case NAME -> String.class;
      case AGE -> Integer.class;
    };
  }

  @Override
  public Object value(Person person, PersonColumn column) {
    return switch (column) {
      case NAME -> person.name();
      case AGE -> person.age();
    };
  }
}

public static FilterTableModel<Person, PersonColumn> createFilterTableModel() {
  // Implement an item supplier responsible for supplying
  // the table items when the table data is refreshed.
  // Without one the table can be populated by adding items manually
  Supplier<Collection<Person>> supplier = () -> List.of(
          new Person("John", 42),
          new Person("Mary", 43));

  // Create the table model
  FilterTableModel<Person, PersonColumn> tableModel =
          FilterTableModel.builder(new PersonColumns())
                  .supplier(supplier)
                  .build();

  // Populate the model
  tableModel.refresh();

  // Select the first row
  tableModel.selection().index().set(0);

  return tableModel;
}

2.4. Common UI

2.4.1. Table UI

FilterTable
filter table diagram

The FilterTable is a JTable subclass central to the framework.

// See FilterTableModel example
FilterTableModel<Person, PersonColumn> tableModel = createFilterTableModel();

List<FilterTableColumn<PersonColumn>> columns = List.of(
        FilterTableColumn.builder(PersonColumn.NAME)
                .headerValue("Name")
                .build(),
        FilterTableColumn.builder(PersonColumn.AGE)
                .headerValue("Age")
                .build());

FilterTable<Person, PersonColumn> filterTable = FilterTable.builder(tableModel, columns)
        .doubleClickAction(Control.command(() ->
                tableModel.selection().item().optional()
                        .ifPresent(System.out::println)))
        .autoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS)
        .build();

// Search for the value "43" in the table
FilterTableSearchModel searchModel = filterTable.searchModel();
searchModel.predicate().set(value -> value.equals("43"));

FilterTableSearchModel.RowColumn searchResult = searchModel.currentResult().get();
System.out.println(searchResult); // row: 1, column: 1

2.4.2. Input Controls

Control
control diagram
State somethingEnabledState = State.state(true);

CommandControl control = Control.builder()
        .command(() -> System.out.println("Doing something"))
        .name("Do something")
        .mnemonic('D')
        .enabled(somethingEnabledState)
        .build();

JButton somethingButton = new JButton(control);

Control.ActionCommand actionCommand = actionEvent -> {
  if ((actionEvent.getModifiers() & ActionEvent.SHIFT_MASK) != 0) {
    System.out.println("Doing something else");
  }
};
CommandControl actionControl = Control.builder()
        .action(actionCommand)
        .name("Do something else")
        .mnemonic('S')
        .build();

JButton somethingElseButton = new JButton(actionControl);
ToggleControl
State state = State.state();

ToggleControl toggleStateControl = Control.builder()
        .toggle(state)
        .build();

JToggleButton toggleButton = Components.toggleButton()
        .toggleControl(toggleStateControl)
        .text("Change state")
        .mnemonic('C')
        .build();

Value<Boolean> booleanValue = Value.builder()
        .nonNull(false)
        .build();

ToggleControl toggleValueControl = Control.builder()
        .toggle(booleanValue)
        .build();

JCheckBox checkBox = Components.checkBox()
        .toggleControl(toggleValueControl)
        .text("Change value")
        .mnemonic('V')
        .build();
Controls
Controls controls = Controls.builder()
        .control(Control.builder()
                .command(this::doFirst)
                .name("First")
                .mnemonic('F'))
        .control(Control.builder()
                .command(this::doSecond)
                .name("Second")
                .mnemonic('S'))
        .control(Controls.builder()
                .name("Submenu")
                .control(Control.builder()
                        .command(this::doSubFirst)
                        .name("Sub-first")
                        .mnemonic('b'))
                .control(Control.builder()
                        .command(this::doSubSecond)
                        .name("Sub-second")
                        .mnemonic('u')))
        .build();

JMenu menu = Components.menu(controls).build();

Control firstControl = Control.builder()
        .command(this::doFirst)
        .name("First")
        .mnemonic('F')
        .build();
Control secondControl = Control.builder()
        .command(this::doSecond)
        .name("Second")
        .mnemonic('S')
        .build();

Controls twoControls = Controls.builder()
        .controls(firstControl, secondControl)
        .build();

JPanel buttonPanel = Components.buttonPanel(twoControls).build();

2.4.3. Input Components

Binding model data to UI components is accomplished by linking a Value instance to an instance of its subclass ComponentValue, which represents a value based on an input component.

//a nullable integer value, initialized to 42
Value<Integer> integerValue =
        Value.value(42);

//create a spinner linked to the value
JSpinner spinner =
        Components.integerSpinner(integerValue)
                .build();

//create a NumberField component value, basically doing the same as
//the above, with an extra step to expose the underlying ComponentValue
ComponentValue<Integer, NumberField<Integer>> numberFieldValue =
        Components.integerField()
                //linked to the same value
                .link(integerValue)
                .buildValue();

//fetch the input field from the component value
NumberField<Integer> numberField = numberFieldValue.component();
Text
TextField
Value<String> stringValue = Value.value();

JTextField textField =
        Components.stringField(stringValue)
                .preferredWidth(120)
                .transferFocusOnEnter(true)
                .build();
Value<Character> characterValue = Value.value();

JTextField textField =
        Components.characterField(characterValue)
                .preferredWidth(120)
                .transferFocusOnEnter(true)
                .build();
TextArea
Value<String> stringValue = Value.value();

JTextArea textArea =
        Components.textArea(stringValue)
                .rowsColumns(10, 20)
                .lineWrap(true)
                .build();
Numbers
Integer
Value<Integer> integerValue = Value.value();

NumberField<Integer> integerField =
        Components.integerField(integerValue)
                .valueRange(0, 10_000)
                .groupingUsed(false)
                .build();
Long
Value<Long> longValue = Value.value();

NumberField<Long> longField =
        Components.longField(longValue)
                .groupingUsed(true)
                .build();
Double
Value<Double> doubleValue = Value.value();

NumberField<Double> doubleField =
        Components.doubleField(doubleValue)
                .maximumFractionDigits(3)
                .decimalSeparator('.')
                .build();
BigDecimal
Value<BigDecimal> bigDecimalValue = Value.value();

NumberField<BigDecimal> bigDecimalField =
        Components.bigDecimalField(bigDecimalValue)
                .maximumFractionDigits(2)
                .groupingSeparator('.')
                .decimalSeparator(',')
                .build();
Date & Time
LocalTime
Value<LocalTime> localTimeValue = Value.value();

TemporalField<LocalTime> temporalField =
        Components.localTimeField(localTimeValue)
                .dateTimePattern("HH:mm:ss")
                .build();
LocalDate
Value<LocalDate> localDateValue = Value.value();

TemporalField<LocalDate> temporalField =
        Components.localDateField(localDateValue)
                .dateTimePattern("dd-MM-yyyy")
                .build();
LocalDateTime
Value<LocalDateTime> localDateTimeValue = Value.value();

TemporalField<LocalDateTime> temporalField =
        Components.localDateTimeField(localDateTimeValue)
                .dateTimePattern("dd-MM-yyyy HH:mm")
                .build();
Boolean
CheckBox
//non-nullable so use this value instead of null
boolean nullValue = false;

Value<Boolean> booleanValue =
        Value.builder()
                .nonNull(nullValue)
                .value(true)
                .build();

JCheckBox checkBox =
        Components.checkBox(booleanValue)
                .text("Check")
                .horizontalAlignment(SwingConstants.CENTER)
                .build();
NullableCheckBox
//nullable boolean value
Value<Boolean> booleanValue = Value.value();

NullableCheckBox checkBox =
        (NullableCheckBox) Components.checkBox(booleanValue)
                .text("Check")
                .nullable(true)
                .build();
ComboBox
Value<Boolean> booleanValue = Value.value();

JComboBox<Item<Boolean>> comboBox =
        Components.booleanComboBox(booleanValue)
                .toolTipText("Select a value")
                .build();
Selection
ComboBox
Value<String> stringValue = Value.value();

DefaultComboBoxModel<String> comboBoxModel =
        new DefaultComboBoxModel<>(new String[] {"one", "two", "three"});

JComboBox<String> comboBox =
        Components.comboBox(comboBoxModel, stringValue)
                .preferredWidth(160)
                .build();
Custom
TextField

In the following example we link a value based on a Person class to a component value displaying text fields for a first and last name.

class Person {
  final String firstName;
  final String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return lastName + ", " + firstName;
  }
}

class PersonPanel extends JPanel {
  final JTextField firstNameField = new JTextField();
  final JTextField lastNameField = new JTextField();

  public PersonPanel() {
    setLayout(new GridLayout(2, 2));
    add(new JLabel("First name"));
    add(new JLabel("Last name"));
    add(firstNameField);
    add(lastNameField);
  }
}

class PersonPanelValue extends AbstractComponentValue<Person, PersonPanel> {

  public PersonPanelValue(PersonPanel component) {
    super(component);
    //We must call notifyListeners() each time this value changes,
    //that is, when either the first or last name changes.
    component.firstNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyListeners());
    component.lastNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyListeners());
  }

  @Override
  protected Person getComponentValue() {
    return new Person(component().firstNameField.getText(), component().lastNameField.getText());
  }

  @Override
  protected void setComponentValue(Person value) {
    component().firstNameField.setText(value == null ? null : value.firstName);
    component().lastNameField.setText(value == null ? null : value.lastName);
  }
}

Value<Person> personValue = Value.value();

PersonPanel personPanel = new PersonPanel();

Value<Person> personPanelValue = new PersonPanelValue(personPanel);

personPanelValue.link(personValue);

2.4.4. Examples

2.5. Common Utilities

Codion provides a few classes with miscellanous utility functions.

2.5.1. Model