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 on the Entity interface, which represents a row in a table. To define a domain model start by creating constants representing the following:

Domain

DomainType

Table

EntityType

Column

Attribute

ForeignKey

ForeignKey

These constants represent the domain API and are used when referring to tables, columns or foreign keys. These are wrapped in an interface denoting the entity type, for a convenient namespace.

public interface Store {

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

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

    Attribute<Integer> ID = TYPE.integerAttribute("id");
    Attribute<String> NAME = TYPE.stringAttribute("name");
  }

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

    Attribute<Integer> ID = TYPE.integerAttribute("id");
    Attribute<String> NAME = TYPE.stringAttribute("name");
    Attribute<Integer> CITY_ID = TYPE.integerAttribute("city_id");

    ForeignKey CITY_FK = TYPE.foreignKey("city", CITY_ID, City.ID);
  }
}

The domain model is implemented by extending the DefaultDomain class, where we define the domain entities and their properties based on the aformentioned API constants and add them to the domain.

public static class StoreImpl extends DefaultDomain {

  public StoreImpl() {
    super(Store.DOMAIN);
    city();
    customer();
  }

  void city() {
    add(definition(
            primaryKeyProperty(City.ID),
            columnProperty(City.NAME, "Name")));
  }

  void customer() {
    add(definition(
            primaryKeyProperty(Customer.ID),
            columnProperty(Customer.NAME, "Name"),
            columnProperty(Customer.CITY_ID),
            foreignKeyProperty(Customer.CITY_FK, "City")));
  }
}

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

Domain store = new StoreImpl();
Entities entities = store.entities();

EntityDefinition customerDefinition = entities.definition(Customer.TYPE);
EntityDefinition cityDefinition = customerDefinition.referencedEntityDefinition(Customer.CITY_FK);
List<Attribute<?>> cityPrimaryKeyAttributes = cityDefinition.primaryKeyAttributes();

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

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();
Entities

The Entity class is a map like structure, mapping column values to the attribute representing each column. Each table is represented by a EntityType constant with a name unique within the domain.

Note
Typically, the entityType name contains the underlying table name, but you can use whatever identifying string you want and specify the table name via the tableName() builder method when defining the entity.
Attributes

Each column in a table is associated with a Attribute constant, which has a name and denotes the value type. The EntityType class provides factory methods for constructing Attribute instances for that entity type.

Note
Typically, the attribute name is the underlying column name, but as with the entityType you can use whatever value you want and specify the column name via the columnName() method when defining the associated Property.
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

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

Each foreign key of a table is associated with ForeignKey constant, which has a name and specifies which column attributes the foreign key is based on.

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");

  Attribute<Integer> ID_1 = TYPE.integerAttribute("id1");
  Attribute<Integer> ID_2 = TYPE.integerAttribute("id2");
}

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

  Attribute<Integer> PARENT_ID_1 = TYPE.integerAttribute("parent_id1");
  Attribute<Integer> PARENT_ID_2 = TYPE.integerAttribute("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 (simplified).

public interface World {

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

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

    Attribute<Integer> ID = TYPE.integerAttribute("id");
    Attribute<String> NAME = TYPE.stringAttribute("name");
    Attribute<String> COUNTRY_CODE = TYPE.stringAttribute("countrycode");
    Attribute<String> DISTRICT = TYPE.stringAttribute("district");
    Attribute<Integer> POPULATION = TYPE.integerAttribute("population");

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

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

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

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

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

    Attribute<String> COUNTRY_CODE = TYPE.stringAttribute("countrycode");
    Attribute<String> LANGUAGE = TYPE.stringAttribute("language");
    Attribute<Boolean> IS_OFFICIAL = TYPE.booleanAttribute("isofficial");
    Attribute<Double> PERCENTAGE = TYPE.doubleAttribute("percentage");
    Attribute<Integer> NO_OF_SPEAKERS = TYPE.integerAttribute("noOfSpeakers");

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

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

    Attribute<String> NAME = TYPE.stringAttribute("continent");
    Attribute<Integer> SURFACE_AREA = TYPE.integerAttribute("sum(surfacearea)");
    Attribute<Long> POPULATION = TYPE.longAttribute("sum(population)");
    Attribute<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleAttribute("min(lifeexpectancy)");
    Attribute<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleAttribute("max(lifeexpectancy)");
    Attribute<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerAttribute("min(indepyear)");
    Attribute<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerAttribute("max(indepyear)");
    Attribute<Double> GNP = TYPE.doubleAttribute("sum(gnp)");
  }
}
Properties

Each column is represented by the Property class or one of its subclasses. The Properties class provides factory methods for constructing Property objects and is usually statically imported, as will be assumed in the following examples. Each Property is based on an Attribute or ForeignKey.

Property

Property and its subclasses are used to represent entity properties, these can be transient or based on table columns.

Primary key

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

The only requirement is that the primary key properties represent a unique column combination for the underlying table, it does not have to correspond to an actual table primary (or unique) key, although that is of course preferable.

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

primaryKeyProperty(Country.CODE, "Code")
        .updatable(true)
        .maximumLength(3),

In case of composite primary keys you create a ColumnProperty and specify the primary key index.

columnProperty(CountryLanguage.COUNTRY_CODE)
        .primaryKeyIndex(0)
        .updatable(true),
columnProperty(CountryLanguage.LANGUAGE, "Language")
        .primaryKeyIndex(1)
        .updatable(true),
ColumnProperty

ColumnProperty is used to represent properties that are based on table columns.

columnProperty(Country.REGION, "Region")
        .nullable(false)
        .maximumLength(26),
columnProperty(Country.SURFACEAREA, "Surface area")
        .nullable(false)
        .numberFormatGrouping(true)
        .maximumFractionDigits(2),
columnProperty(Country.INDEPYEAR, "Indep. year")
        .valueRange(-2000, 2500),
columnProperty(Country.INDEPYEAR_SEARCHABLE)
        .columnExpression("to_char(indepyear)")
        .searchProperty(true)
        .readOnly(true),
columnProperty(Country.POPULATION, "Population")
        .nullable(false)
        .numberFormatGrouping(true),
columnProperty(Country.LIFE_EXPECTANCY, "Life expectancy")
        .maximumFractionDigits(1)
        .valueRange(0, 99),
Subquery

A ColumnProperty can represent a subquery returning a single value.

subqueryProperty(Country.NO_OF_CITIES, "No. of cities",
        "select count(*) from world.city " +
                "where city.countrycode = country.code"),
BlobProperty

BlobProperty is a specific ColumnProperty for the Types.BLOB type which provides lazy loading for BLOB values. Using a standard ColumnProperty with a byte array type works fine, but for lazy loading you have to use BlobProperty.

blobProperty(Country.FLAG, "Flag")
        .eagerlyLoaded(true),
ForeignKeyProperty

ForeignKeyProperty is a property used to represent a foreign key relation.

foreignKeyProperty(Country.CAPITAL_FK, "Capital"),
Boolean Properties

For databases supporting Types.BOOLEAN you simply use Properties.columnProperty.

columnProperty(CountryLanguage.IS_OFFICIAL, "Official")
        .columnHasDefaultValue(true)
        .nullable(false),

For databases lacking native boolean support we use the Properties.booleanProperty method, specifying the underlying type and the true/false values.

booleanProperty(Customer.IS_ACTIVE, "Is active", Integer.class, 1, 0)
booleanProperty(Customer.IS_ACTIVE, "Is active", String.class, "true", "false")
booleanProperty(Customer.IS_ACTIVE, "Is active", Character.class, 'T', 'F')

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

entity.put(Customer.IS_ACTIVE, true);

Boolean isActive = entity.get(Customer.IS_ACTIVE);
ItemProperty

A property 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"));
itemProperty(Country.CONTINENT, "Continent", CONTINENT_ITEMS)
        .nullable(false),
DenormalizedViewProperty

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

denormalizedViewProperty(Country.CAPITAL_POPULATION, "Capital pop.",
        Country.CAPITAL_FK, City.POPULATION)
        .numberFormatGrouping(true),
DenormalizedProperty

DenormalizedProperty is used for columns that should automatically get their value from a column in a referenced table. This property automatically gets the value from the attribute in the referenced table when the corresponding reference property value is set.

denormalizedProperty(Customer.CITY, Customer.ADDRESS_FK, Address.CITY, "City")
Note
The property is not kept in sync if the value of the denormalized property is modified in the referenced entity.
Entities entities = getEntities();

Entity address = entities.entity(Address.TYPE);
address.put(Address.CITY, "Syracuse");

Entity customer = entities.entity(Customer.TYPE);
customer.put(Customer.ADDRESS_FK, address);

customer.get(Customer.CITY);//returns "Syracuse"

//NB
address.put(Address.CITY, "Canastota");
customer.get(Customer.CITY, still returns "Syracuse"

customer.put(Customer.ADDRESS_FK, address);//set the referenced value again
customer.get(Customer.CITY);//now this returns "Canastota"
TransientProperty

TransientProperty is used to represent a property which is not based on an underlying column, these properties all have a default value of null and can be set and retrieved just like normal properties.

DerivedProperty

DerivedProperty is used to represent a transient property which value is derived from one or more properties in the same entity. The value of a derived property is provided via a DerivedProperty.Provider implementation as shown below.

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

  private static final long serialVersionUID = 1;

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

    return null;
  }
}
Domain

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

void city() {
  add(definition(
          primaryKeyProperty(City.ID),
          columnProperty(City.NAME, "Name")
                  .searchProperty(true)
                  .nullable(false)
                  .maximumLength(35),
          columnProperty(City.COUNTRY_CODE)
                  .nullable(false),
          foreignKeyProperty(City.COUNTRY_FK, "Country"),
          columnProperty(City.DISTRICT, "District")
                  .nullable(false)
                  .maximumLength(20),
          columnProperty(City.POPULATION, "Population")
                  .nullable(false)
                  .numberFormatGrouping(true),
          columnProperty(City.LOCATION, "Location")
                  .columnClass(String.class, new LocationConverter())
                  .comparator(new LocationComparator()))
          .keyGenerator(sequence("world.city_seq"))
          .validator(new CityValidator())
          .orderBy(ascending(City.NAME))
          .stringFactory(City.NAME)
          .foregroundColorProvider(new CityColorProvider())
          .description("Cities of the World")
          .caption("City"));
}
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'"));
Increment

The framework can automatically increment the primary key value by selecting the maximum value and add one, this is very simplistic, not transaction safe and is not recommended for use anywhere but the simplest demos.

.keyGenerator(increment("scott.emp", "empno"))
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, List<ColumnProperty<?>> primaryKeyProperties,
                           DatabaseConnection connection) throws SQLException {
    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.

add(definition(
        primaryKeyProperty(Address.ID),
        columnProperty(Address.STREET, "Street")
                .nullable(false)
                .maximumLength(120),
        columnProperty(Address.CITY, "City")
                .nullable(false)
                .maximumLength(50),
        columnProperty(Address.VALID, "Valid")
                .columnHasDefaultValue(true)
                .nullable(false))
        .stringFactory(StringFactory.builder()
                .value(Address.STREET)
                .text(", ")
                .value(Address.CITY)
                .build())
        .keyGenerator(identity())
        .smallDataset(true)
        .caption("Address"));

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 {

  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();
  }
}
ColorProvider

ColorProvider is used to provide colors for entity properties, used as foreground or background color in table cells for example.

The framework supports String color definitions, in a format recognized by java.awt.Color.decode(), like the one below, in case you don’t want a dependency on java.awt.Color in the domain model.

final class CityColorProvider implements ColorProvider {

  private static final long serialVersionUID = 1;

  private static final String YELLOW = "#ffff00";
  private static final String GREEN = "#00ff00";

  @Override
  public Object color(Entity cityEntity, Attribute<?> attribute) {
    if (attribute.equals(City.POPULATION) &&
            cityEntity.get(City.POPULATION) > 1_000_000) {
      return YELLOW;
    }
    City city = cityEntity.castTo(City.class);
    if (attribute.equals(City.NAME) && city.isCapital()) {
      return GREEN;
    }

    return null;
  }
}
.foregroundColorProvider(new CityColorProvider())
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 {

  private static final long serialVersionUID = 1;

  @Override
  public void validate(Entity city) throws ValidationException {
    super.validate(city);
    //after a call to super.validate() property 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 ColumnProperty and provide a ValueConverter implementation.

Attribute<Location> LOCATION = TYPE.attribute("location", Location.class);
final class Location implements Serializable {

  private static final long serialVersionUID = 1;

  private final double latitude;
  private final double longitude;

  public Location(double latitude, double longitude) {
    this.latitude = latitude;
    this.longitude = longitude;
  }

  public double latitude() {
    return latitude;
  }

  public double longitude() {
    return longitude;
  }

  @Override
  public String toString() {
    return "[" + latitude + ", " + longitude + "]";
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) {
      return true;
    }
    if (other == null || getClass() != other.getClass()) {
      return false;
    }
    Location location = (Location) other;

    return Double.compare(location.latitude, latitude) == 0
            && Double.compare(location.longitude, longitude) == 0;
  }

  @Override
  public int hashCode() {
    return Objects.hash(latitude, longitude);
  }
}
columnProperty(City.LOCATION, "Location")
        .columnClass(String.class, new LocationConverter())
        .comparator(new LocationComparator()))
private static final class LocationConverter implements ValueConverter<Location, String> {

  @Override
  public String toColumnValue(Location location,
                              Statement statement) throws SQLException {
    if (location == null) {
      return null;
    }

    return "POINT (" + location.longitude() + " " + location.latitude() + ")";
  }

  @Override
  public Location fromColumnValue(String columnValue) throws SQLException {
    if (columnValue == null) {
      return null;
    }

    String[] latLon = columnValue
            .replace("POINT (", "")
            .replace(")", "")
            .split(" ");

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

This custom type must be serializable for use in an application using the RMI connection.

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

public final class WorldObjectMapperFactory extends DefaultEntityObjectMapperFactory {

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

  @Override
  public EntityObjectMapper entityObjectMapper(Entities entities) {
    StdSerializer<Location> locationSerializer = new StdSerializer<Location>(Location.class) {
      @Override
      public void serialize(Location value, JsonGenerator generator,
                            SerializerProvider provider) throws IOException {
        generator.writeStartObject();
        generator.writeNumberField("lat", value.latitude());
        generator.writeNumberField("lon", value.longitude());
        generator.writeEndObject();
      }
    };

    StdDeserializer<Location> locationDeserializer = new StdDeserializer<Location>(Location.class) {
      @Override
      public Location deserialize(JsonParser parser, DeserializationContext ctxt)
              throws IOException, JsonProcessingException {
        JsonNode node = parser.getCodec().readTree(parser);

        return new Location(node.get("lat").asDouble(), node.get("lon").asDouble());
      }
    };

    EntityObjectMapper objectMapper = EntityObjectMapper.entityObjectMapper(entities);
    objectMapper.addSerializer(Location.class, locationSerializer);
    objectMapper.addDeserializer(Location.class, locationDeserializer);

    return objectMapper;
  }
}

This EntityObjectMapperFactory must be exposed to the ServiceLoader.

src/main/resources/META-INF/services/is.codion.plugin.jackson.json.domain.EntityObjectMapperFactory

is.codion.framework.demos.world.domain.api.WorldObjectMapperFactory
Entities in action

Using the Entity class is rather straight forward.

EntityConnectionProvider connectionProvider = EntityConnectionProvider.builder()
        .domainClassName(Petstore.class.getName())
        .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();

connection.insert(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, "Cats");

List<Entity> catProducts = connection.select(Product.CATEGORY_FK, categoryCats);

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

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

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.

EntityTestUnit

The following methods all have default implementations which are based on randomly created property values, based on the constraints set in the domain model, override if the default ones are not working.

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

  • initializeTestEntity 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.

  • modifyEntity 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 modifyEntity 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 EntityTestUnit {

  public StoreTest() {
    super(Store.class.getName());
  }

  @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);
  }

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

    return super.initializeForeignKeyEntity(foreignKey, foreignKeyEntities);
  }

  @Override
  protected Entity initializeTestEntity(EntityType entityType,
                                        Map<ForeignKey, Entity> foreignKeyEntities) {
    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")
              .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.IS_ACTIVE, true)
              .build();
    }
    else if (entityType.equals(CustomerAddress.TYPE)) {
      return entities().builder(CustomerAddress.TYPE)
              .with(CustomerAddress.CUSTOMER_FK, foreignKeyEntities.get(Customer.TYPE))
              .with(CustomerAddress.ADDRESS_FK, foreignKeyEntities.get(Address.TYPE))
              .build();
    }

    return super.initializeTestEntity(entityType, foreignKeyEntities);
  }

  @Override
  protected void modifyEntity(Entity testEntity, Map<ForeignKey, Entity> foreignKeyEntities) {
    if (testEntity.type().equals(Address.TYPE)) {
      testEntity.put(Address.STREET, "New Street");
      testEntity.put(Address.CITY, "New City");
    }
    else if (testEntity.type().equals(Customer.TYPE)) {
      //It is sufficient to change the value of a single property, but the more, the merrier
      testEntity.put(Customer.FIRST_NAME, "Jesse");
      testEntity.put(Customer.LAST_NAME, "James");
      testEntity.put(Customer.IS_ACTIVE, false);
    }
  }
}

1.1.2. Conditions

The Conditions class is a factory for conditions, used when querying entities.

The Chinook domain model is used in the examples below.

Condition

Condition represents a where clause for an entity type.

Condition.Combination

Condition.Combination represents a combination of Conditions, which are either AND’ed or OR’ed together. These can be nested and combined with other condition combinations.

SelectCondition

SelectCondition represents a where clause for an entity type specifically for selecting.

UpdateCondition

UpdateCondition represents a where clause and values for updating one or more entities.

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.

Note
Static imports are used extensively in the examples, see full example code.
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 foreign key reference is not 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. This limiting of foreign key fetch depth can be turned off, meaning the full reference graph is always fetched, via the codion.db.limitForeignKeyFetchDepth=false system property or LocalEntityConnection.LIMIT_FOREIGN_KEY_FETCH_DEPTH.set(false), or on a connection instance via connection.setLimitFetchDepth(false).

foreignKeyProperty(InvoiceLine.INVOICE_FK)
        .fetchDepth(0),
foreignKeyProperty(Track.ALBUM_FK)
        .selectAttributes(Album.ARTIST_FK, Album.TITLE)
        .fetchDepth(2)
        .preferredColumnWidth(160),
List<Entity> tracks = connection.select(Track.NAME, "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.

List<Entity> tracks = connection.select(
        where(Track.NAME).equalTo("Bad%")
                .selectBuilder()
                .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.getForeignKey(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.referencedEntity(Track.GENRE_FK);
List<Entity> tracks = connection.select(
        where(Track.NAME).equalTo("Bad%")
                .selectBuilder()
                .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.getForeignKey(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.referencedEntity(Track.ALBUM_FK);
Selecting entities
List<Entity> artists = connection.select(
        where(Artist.NAME).equalTo("The %"));

List<Entity> nonLiveAlbums = connection.select(
        where(Album.ARTIST_FK).equalTo(artists)
                .and(where(Album.TITLE).notEqualToIgnoreCase("%live%")));
Entities entities = connection.entities();

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

Entity artists = connection.select(key42);
Entities entities = connection.entities();

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

List<Entity> artists = connection.select(asList(key42, key43));
Entity aliceInChains =
        connection.selectSingle(Artist.NAME, "Alice In Chains");

List<Entity> albums =
        connection.select(Album.ARTIST_FK, aliceInChains);
Entity ironMaiden = connection.selectSingle(
        where(Artist.NAME).equalTo("Iron Maiden"));

Entity liveAlbum = connection.selectSingle(
        where(Album.ARTIST_FK).equalTo(ironMaiden)
                .and(where(Album.TITLE).equalToIgnoreCase("%live after%")));
Entity aliceInChains = connection.selectSingle(Artist.NAME, "Alice In Chains");

// we only have one album by Alice in Chains
// otherwise this would throw an exception
Entity albumFacelift = connection.selectSingle(Album.ARTIST_FK, aliceInChains);
Selecting values

For selecting the values of a single column.

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

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

LocalEntityConnection localConnection = (LocalEntityConnection) connection;

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

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

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

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

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

For selecting the row count given a condition.

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

For inserting rows.

Entities entities = connection.entities();

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

connection.insert(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();

List<Key> keys = 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.

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

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

connection.update(myBand);

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

Entity.put(Customer.PHONE, "<none>", customersWithoutPhoneNo);

connection.update(customersWithoutPhoneNo);
Optimistic locking

The framework performs optimistic locking during updates of entity instances using the methods above, by selecting the entities being updated for update (when supported by the underlying database) and comparing all the original property values to the current row values, throwing an exception if any value differs or the row is missing. Optimistic locking can be turned off system-wide using the codion.db.useOptimisticLocking=false system property or LocalEntityConnection.USE_OPTIMISTIC_LOCKING.set(false), or on a connection instance via connection.setOptimisticLockingEnabled(false).

For updating by where condition.

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

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

For deleting existing rows.

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

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

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

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

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

int albumsDeleted = connection.delete(
        where(Album.ARTIST_FK).equalTo(aquaman));
Entity audioslave = connection.selectSingle(Artist.NAME, "Audioslave");

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

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

connection.delete(toDelete);
Procedures & Functions
executeFunction
List<Long> trackIds = asList(123L, 1234L);
BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

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

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

String playlistName = "Random playlist";
int numberOfTracks = 100;

Entity playlist = connection.executeFunction(Playlist.RANDOM_PLAYLIST,
        new RandomPlaylistParameters(playlistName, numberOfTracks));
executeProcedure

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

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

JasperPrint jasperPrint = connection.fillReport(Customer.REPORT, reportParameters);
Full example
Show code
package is.codion.framework.demos.chinook.manual;

import is.codion.common.db.database.Database;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.report.ReportException;
import is.codion.common.db.result.ResultIterator;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnection;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.demos.chinook.domain.Chinook.Playlist.RandomPlaylistParameters;
import is.codion.framework.demos.chinook.domain.Chinook.Track.RaisePriceParameters;
import is.codion.framework.demos.chinook.domain.impl.ChinookImpl;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.Key;

import net.sf.jasperreports.engine.JasperPrint;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static is.codion.framework.db.condition.Conditions.condition;
import static is.codion.framework.db.condition.Conditions.where;
import static is.codion.framework.demos.chinook.domain.Chinook.*;
import static java.util.Arrays.asList;

/**
 * When running this make sure the chinook demo module directory is the
 * working directory, due to a relative path to a db init script
 */
public final class EntityConnectionDemo {

  static void selectConditionDemo(EntityConnection connection) throws DatabaseException {
    // tag::selectCondition[]
    List<Entity> artists = connection.select(
            where(Artist.NAME).equalTo("The %"));

    List<Entity> nonLiveAlbums = connection.select(
            where(Album.ARTIST_FK).equalTo(artists)
                    .and(where(Album.TITLE).notEqualToIgnoreCase("%live%")));
    // end::selectCondition[]
  }

  static void fetchDepthEntity(EntityConnection connection) throws DatabaseException {
    // tag::fetchDepthEntity[]
    List<Entity> tracks = connection.select(Track.NAME, "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);
    // end::fetchDepthEntity[]
  }

  static void fetchDepthCondition(EntityConnection connection) throws DatabaseException {
    // tag::fetchDepthCondition[]
    List<Entity> tracks = connection.select(
            where(Track.NAME).equalTo("Bad%")
                    .selectBuilder()
                    .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.getForeignKey(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.referencedEntity(Track.GENRE_FK);
    // end::fetchDepthCondition[]
  }

  static void fetchDepthForeignKeyCondition(EntityConnection connection) throws DatabaseException {
    // tag::fetchDepthConditionForeignKey[]
    List<Entity> tracks = connection.select(
            where(Track.NAME).equalTo("Bad%")
                    .selectBuilder()
                    .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.getForeignKey(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.referencedEntity(Track.ALBUM_FK);
    // end::fetchDepthConditionForeignKey[]
  }

  static void selectKeys(EntityConnection connection) throws DatabaseException {
    // tag::selectKeys[]
    Entities entities = connection.entities();

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

    List<Entity> artists = connection.select(asList(key42, key43));
    // end::selectKeys[]
  }

  static void selectByValue(EntityConnection connection) throws DatabaseException {
    // tag::selectByValue[]
    Entity aliceInChains =
            connection.selectSingle(Artist.NAME, "Alice In Chains");

    List<Entity> albums =
            connection.select(Album.ARTIST_FK, aliceInChains);
    // end::selectByValue[]
  }

  static void selectSingleCondition(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleCondition[]
    Entity ironMaiden = connection.selectSingle(
            where(Artist.NAME).equalTo("Iron Maiden"));

    Entity liveAlbum = connection.selectSingle(
            where(Album.ARTIST_FK).equalTo(ironMaiden)
                    .and(where(Album.TITLE).equalToIgnoreCase("%live after%")));
    // end::selectSingleCondition[]
  }

  static void selectKey(EntityConnection connection) throws DatabaseException {
    // tag::selectKey[]
    Entities entities = connection.entities();

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

    Entity artists = connection.select(key42);
    // end::selectKey[]
  }

  static void selectSingleValue(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleValue[]
    Entity aliceInChains = connection.selectSingle(Artist.NAME, "Alice In Chains");

    // we only have one album by Alice in Chains
    // otherwise this would throw an exception
    Entity albumFacelift = connection.selectSingle(Album.ARTIST_FK, aliceInChains);
    // end::selectSingleValue[]
  }

  static void selectValues(EntityConnection connection) throws DatabaseException {
    // tag::selectValues[]
    List<String> customerUsStates =
            connection.select(Customer.STATE,
                    where(Customer.COUNTRY).equalTo("USA"));
    // end::selectValues[]
  }

  static void selectDependencies(EntityConnection connection) throws DatabaseException {
    // tag::selectDependencies[]
    List<Entity> employees = connection.select(condition(Employee.TYPE));

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

    Collection<Entity> customersDependingOnEmployees = dependencies.get(Customer.TYPE);
    // end::selectDependencies[]
  }

  static void rowCount(EntityConnection connection) throws DatabaseException {
    // tag::rowCount[]
    int numberOfItStaff = connection.rowCount(where(Employee.TITLE).equalTo("IT Staff"));
    // end::rowCount[]
  }

  static void insert(EntityConnection connection) throws DatabaseException {
    // tag::insert[]
    Entities entities = connection.entities();

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

    connection.insert(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();

    List<Key> keys = connection.insert(asList(firstAlbum, secondAlbum));
    // end::insert[]
  }

  static void update(EntityConnection connection) throws DatabaseException {
    // tag::update[]
    Entity myBand = connection.selectSingle(Artist.NAME, "My Band");

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

    connection.update(myBand);

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

    Entity.put(Customer.PHONE, "<none>", customersWithoutPhoneNo);

    connection.update(customersWithoutPhoneNo);
    // end::update[]
  }

  static void updateConditionDemo(EntityConnection connection) throws DatabaseException {
    // tag::updateCondition[]
    connection.update(
            where(Artist.NAME).equalTo("Azymuth")
                    .updateBuilder()
                    .set(Artist.NAME, "Azymouth")
                    .build());

    int updateCount = connection.update(
            where(Customer.EMAIL).isNull()
                    .updateBuilder()
                    .set(Customer.EMAIL, "<none>")
                    .build());
    // end::updateCondition[]
  }

  static void deleteCondition(EntityConnection connection) throws DatabaseException {
    // tag::deleteCondition[]
    Entity aquaman = connection.selectSingle(Artist.NAME, "Aquaman");

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

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

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

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

    int albumsDeleted = connection.delete(
            where(Album.ARTIST_FK).equalTo(aquaman));
    // end::deleteCondition[]
  }

  static void deleteKey(EntityConnection connection) throws DatabaseException {
    // tag::deleteKey[]
    Entity audioslave = connection.selectSingle(Artist.NAME, "Audioslave");

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

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

    connection.delete(toDelete);
    // end::deleteKey[]
  }

  static void iterator(EntityConnection connection) throws DatabaseException {
    // tag::iterator[]
    LocalEntityConnection localConnection = (LocalEntityConnection) connection;

    try (ResultIterator<Entity> iterator =
                 localConnection.iterator(where(Customer.EMAIL).isNotNull())) {
      while (iterator.hasNext()) {
        System.out.println(iterator.next().get(Customer.EMAIL));
      }
    }
    catch (SQLException e) {
      throw new DatabaseException(e);
    }
    // end::iterator[]
  }

  static void function(EntityConnection connection) throws DatabaseException {
    // tag::function[]
    List<Long> trackIds = asList(123L, 1234L);
    BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

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

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

    String playlistName = "Random playlist";
    int numberOfTracks = 100;

    Entity playlist = connection.executeFunction(Playlist.RANDOM_PLAYLIST,
            new RandomPlaylistParameters(playlistName, numberOfTracks));
    // end::function[]
  }

  static void report(EntityConnection connection) throws ReportException, DatabaseException {
    // tag::report[]
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", asList(42, 43, 45));

    JasperPrint jasperPrint = connection.fillReport(Customer.REPORT, reportParameters);
    //end::report[]
  }

  static void transaction(EntityConnection connection) throws DatabaseException {
    // tag::transaction[]
    Entities entities = connection.entities();

    connection.beginTransaction();
    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 (Exception e) {
      connection.rollbackTransaction();
      throw new RuntimeException(e);
    }
    // end::transaction[]
  }

  public static void main(String[] args) throws DatabaseException, ReportException {
    Database.DATABASE_URL.set("jdbc:h2:mem:h2db");
    Database.DATABASE_INIT_SCRIPTS.set("src/main/sql/create_schema.sql");

    EntityConnectionProvider connectionProvider =
            LocalEntityConnectionProvider.builder()
                    .domainClassName(ChinookImpl.class.getName())
                    .user(User.parse("scott:tiger"))
                    .build();

    EntityConnection connection = connectionProvider.connection();
    selectConditionDemo(connection);
    fetchDepthEntity(connection);
    fetchDepthCondition(connection);
    fetchDepthForeignKeyCondition(connection);
    selectKeys(connection);
    selectByValue(connection);
    iterator(connection);
    selectSingleCondition(connection);
    selectKey(connection);
    selectSingleValue(connection);
    selectValues(connection);
    selectDependencies(connection);
    rowCount(connection);
    insert(connection);
    update(connection);
    updateConditionDemo(connection);
    deleteCondition(connection);
    deleteKey(connection);
    function(connection);
    report(connection);
    transaction(connection);
  }
}
Transaction control
Entities entities = connection.entities();

connection.beginTransaction();
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 (Exception e) {
  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()
                .domainClassName(ChinookImpl.class.getName())
                .user(User.parse("scott:tiger"))
                .build();

LocalEntityConnection entityConnection =
        (LocalEntityConnection) 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()
                .domainClassName(ChinookImpl.class.getName())
                .user(User.parse("scott:tiger"))
                .clientTypeId(EntityConnectionProviderDemo.class.getSimpleName())
                .serverHostName("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()
                .domainClassName(ChinookImpl.class.getName())
                .clientTypeId(EntityConnectionProviderDemo.class.getSimpleName())
                .user(User.parse("scott:tiger"))
                .serverHostName("localhost")
                .serverPort(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 EntityModel class links together and coordinates between a EntityEditModel and a EntityTableModel, where the EntityEditModel handles CRUD operations and the EntityTableModel provides a table representation of the underlying entities.

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().refreshingObserver().addDataListener(refreshing -> {
      if (refreshing) {
        System.out.println("Refresh is about to start");
      }
    });

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

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

Each EntityModel contains a single EntityEditModel instance. This edit model can be created automatically by the EntityModel or supplied via a constructor argument in case of a custom implementation.

public class CustomerEditModel extends SwingEntityEditModel {

  public CustomerEditModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
Table model

Each EntityModel can contain a single EntityTableModel instance. This table model can be created automatically by the EntityModel or supplied via a constructor argument in case of a specialized implementation.

public class CustomerTableModel extends SwingEntityTableModel {

  public CustomerTableModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerEditModel(connectionProvider));
  }
}
Detail models

Detail models can be added to a model, this relies on a foreign key reference between the entities involved.

public class StoreAppModel extends SwingEntityApplicationModel {

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

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

    customerModel.addDetailModel(customerAddressModel);

    addEntityModel(customerModel);
  }
}
Event binding

The EntityModel, EntityEditModel and EntityTableModel classes expose a number of addListener methods.

The following example prints, to the standard output, all changes made to a given property as well as a message indicating that a refresh has started.

private void bindEvents() {
  tableModel().refreshingObserver().addDataListener(refreshing -> {
    if (refreshing) {
      System.out.println("Refresh is about to start");
    }
  });

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

1.2.2. EntityEditModel

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, called the active entity, which can be set via the setEntity(Entity entity) method and retrieved via getEntityCopy(). The EntityEditModel interface exposes a number of methods for manipulating as well as querying the property values of the active entity.

public class CustomerEditModel extends SwingEntityEditModel {

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

CustomerEditModel editModel = new CustomerEditModel(connectionProvider);

editModel.put(Customer.ID, UUID.randomUUID().toString());
editModel.put(Customer.FIRST_NAME, "Björn");
editModel.put(Customer.LAST_NAME, "Sigurðsson");
editModel.put(Customer.IS_ACTIVE, true);

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

//modify some property values
editModel.put(Customer.FIRST_NAME, "John");
editModel.put(Customer.LAST_NAME, "Doe");

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

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

1.2.3. EntityTableModel

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

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 implementing this class you must provide a constructor with a single EntityConnectionProvider parameter, as seen below.

public class StoreAppModel extends SwingEntityApplicationModel {

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

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

    customerModel.addDetailModel(customerAddressModel);

    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 extending the abstract class EntityLoadTestModel.

public class StoreLoadTest extends EntityLoadTestModel<StoreAppModel> {

  public StoreLoadTest(User user) {
    super(user, singletonList(new UsageScenario()));
  }

  @Override
  protected StoreAppModel createApplication() {
    EntityConnectionProvider connectionProvider =
            RemoteEntityConnectionProvider.builder()
                    .clientId(UUID.randomUUID())
                    .user(getUser())
                    .domainClassName(Store.class.getName())
                    .build();

    return new StoreAppModel(connectionProvider);
  }

  private static class UsageScenario extends
          AbstractEntityUsageScenario<StoreAppModel> {

    @Override
    protected void perform(StoreAppModel application)
            throws Exception {
      SwingEntityModel customerModel = application.entityModel(Customer.TYPE);
      customerModel.tableModel().refresh();
      selectRandomRow(customerModel.tableModel());
    }
  }
}

1.3. Framework UI

1.3.1. EntityPanel

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.

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.

1.3.2. EntityEditPanel

The EntityEditPanel contains the controls (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);
    setDefaultTextFieldColumns(15);
  }

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

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

    setLayout(new GridLayout(4,1));
    //the addInputPanel method creates and adds a panel containing the
    //component associated with the property 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.IS_ACTIVE);
  }
}
public class AddressEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(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() {
    setInitialFocusAttribute(CustomerAddress.ADDRESS_FK);

    EntityComboBox addressComboBox =
            createForeignKeyComboBox(CustomerAddress.ADDRESS_FK)
                    .preferredWidth(200)
                    .build();
    Action newAddressAction = EntityPanel.builder(Address.TYPE)
            .editPanelClass(AddressEditPanel.class)
            .createEditPanelAction(addressComboBox);
    JPanel addressPanel = Panels.createEastButtonPanel(addressComboBox, newAddressAction);

    setLayout(new BorderLayout(5, 5));

    addInputPanel(CustomerAddress.ADDRESS_FK, addressPanel);
  }
}
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 =
        createLocalDateField(Demo.LOCAL_DATE)
                .build();

TemporalInputPanel<LocalDate> inputPanel =
        createTemporalInputPanel(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();

TextInputPanel inputPanel =
        createTextInputPanel(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. The table popup menu is based on the Controls returned by createPopupControls() in the EntityTablePanel class, which in turn calls createPrintControls() to populate the print popup submenu controls. So, to add a custom print action you override it and add a custom action to the default Controls instance.

public class CustomerTablePanel extends EntityTablePanel {

  public CustomerTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
  }

  @Override
  protected Controls createPrintControls() {
    Controls printControls = super.createPrintControls();
    //add a Control which calls the viewCustomerReport method in this class
    //enabled only when the selection is not empty
    printControls.add(Control.builder(this::viewCustomerReport)
            .caption("Customer report")
            .enabledState(table().getModel().selectionModel().selectionNotEmptyObserver())
            .build());

    return printControls;
  }

  private void viewCustomerReport() throws Exception {
    List<Entity> selectedCustomers = table().getModel().selectionModel().getSelectedItems();
    if (selectedCustomers.isEmpty()) {
      return;
    }

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

    JasperPrint customerReport = tableModel().connectionProvider().connection()
            .fillReport(Store.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.

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

    return Collections.singletonList(addressPanelBuilder);
  }

1.3.5. EntityApplicationPanel

public class StoreAppPanel extends EntityApplicationPanel<StoreAppModel> {

  public StoreAppPanel() {
    super("Store");
  }

  @Override
  protected List<EntityPanel> createEntityPanels(StoreAppModel applicationModel) {
    CustomerModel customerModel =
            (CustomerModel) applicationModel.entityModel(Customer.TYPE);
    //populate model with rows from database
    customerModel.tableModel().refresh();

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

    CustomerAddressModel customerAddressModel =
            (CustomerAddressModel) customerModel.detailModel(CustomerAddress.TYPE);
    EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
            new CustomerAddressEditPanel(customerAddressModel.editModel()));

    customerPanel.addDetailPanel(customerAddressPanel);

    return Collections.singletonList(customerPanel);
  }

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

    return Collections.singletonList(addressPanelBuilder);
  }

  @Override
  protected StoreAppModel createApplicationModel(EntityConnectionProvider connectionProvider) {
    return new StoreAppModel(connectionProvider);
  }

  public static void main(String[] args) {
    Locale.setDefault(new Locale("en", "EN"));
    EntityEditModel.POST_EDIT_EVENTS.set(true);
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING
            .set(ReferentialIntegrityErrorHandling.DISPLAY_DEPENDENCIES);
    ColumnConditionModel.AUTOMATIC_WILDCARD.set(AutomaticWildcard.POSTFIX);
    ColumnConditionModel.CASE_SENSITIVE.set(false);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.manual.store.domain.Store");
    Report.REPORT_PATH.set("http://test.io");
    SwingUtilities.invokeLater(() -> new StoreAppPanel().starter()
            .frameSize(Windows.screenSizeRatio(0.6))
            .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);
  }

  @Override
  protected Controls createPrintControls() {
    Controls printControls = super.createPrintControls();
    //add a Control which calls the viewCustomerReport method in this class
    //enabled only when the selection is not empty
    printControls.add(Control.builder(this::viewCustomerReport)
            .caption("Customer report")
            .enabledState(table().getModel().selectionModel().selectionNotEmptyObserver())
            .build());

    return printControls;
  }

  private void viewCustomerReport() throws Exception {
    List<Entity> selectedCustomers = table().getModel().selectionModel().getSelectedItems();
    if (selectedCustomers.isEmpty()) {
      return;
    }

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

    JasperPrint customerReport = tableModel().connectionProvider().connection()
            .fillReport(Store.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', 'is_active', 'category_code' etc.

EntityConnection connection = connectionProvider.connection();

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

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

JasperReportsDataSource<Entity> dataSource =
        new JasperReportsDataSource<>(customerIterator,
                (entity, reportField) ->
                        entity.get(customerDefinition.attribute(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 the Event, State and Value classes, their respective observers EventObserver and StateObserver and listeners EventListener and EventDataListener.

Event

The Event class is a simple synchronous event implementation used throughout the framework. Classes typically publish their events via public addListener methods. Events are triggered by calling the onEvent() method, with or without a data parameter.

Events are instantiated via factory methods in the Event class.

To listen to Events you use the EventListener or EventDataListener interfaces.

Event<String> event = Event.event();

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

// add a listener notified each time the event occurs
eventObserver.addListener(() -> System.out.println("Event occurred"));

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

// data can be propagated by adding a EventDataListener
eventObserver.addDataListener(data -> System.out.println("Event: " + data));

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

// Event extends EventObserver so listeneres can be added
// directly without referring to the EventObserver
event.addListener(() -> System.out.println("Event"));
State

The State class encapsulates a boolean state and provides read only access and a change observer via StateObserver.

States are instantiated via factory methods in the State class.

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

// an observer handles the listeners for a State but can not change it
StateObserver stateObserver = state.observer();
// a reversed observer is always available
StateObserver reversedObserver = state.reversedObserver();

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

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

stateObserver.addDataListener(value -> System.out.println("State: " + value));

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

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

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

State state = State.state();

Action action = new AbstractAction("action") {
  public void actionPerformed(ActionEvent e) {}
};

Utilities.linkToEnabledState(state, action);

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

state.set(true);

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

The Value interface is a value wrapper with a change listener.

Values are instantiated via factory methods in the Value class.

Value<Integer> value = Value.value();

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

value.set(2);

NumberField<Integer> integerField =
        Components.integerField(value)
                .build();

integerField.setNumber(3);//linked value is now 3

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.

    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.createDatabase(url);
  • By instantiating a DBMS specific DatabaseFactory directly.

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

    H2DatabaseFactory databaseFactory = new H2DatabaseFactory();

    Database database = databaseFactory.createDatabase(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 an actual java.sql.Connection instance.

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);

java.sql.Connection connection = databaseConnection.getConnection();

2.3. Common UI

2.3.1. Input Controls

Control
State somethingEnabledState = State.state(true);

Control control = Control.builder(() -> System.out.println("Doing something"))
        .caption("Do something")
        .mnemonic('D')
        .enabledState(somethingEnabledState)
        .build();

JButton somethingButton = new JButton(control);

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

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

JToggleButton toggleButton = Components.toggleButton(state)
        .caption("Change state")
        .mnemonic('C')
        .build();

Value<Boolean> booleanValue = Value.value();

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

JMenu menu = controls.createMenu();

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

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

JPanel buttonPanel = twoControls.createHorizontalButtonPanel();

2.3.2. Input Components

Here are the basics of linking input controls to model values.

Text
TextField
Value<String> stringValue = Value.value();

JTextField textField =
        Components.textField(stringValue)
                .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("HH:mm:ss", localTimeValue)
                .build();
LocalDate
Value<LocalDate> localDateValue = Value.value();

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

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

Value<Boolean> booleanValue = Value.value(true, nullValue);

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

NullableCheckBox checkBox =
        (NullableCheckBox) Components.checkBox(booleanValue)
                .caption("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 notifyValueChange() each time this value changes,
    //that is, when either the first or last name changes.
    component.firstNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyValueChange());
    component.lastNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyValueChange());
  }

  @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);
Property

Below we link the 'horizontalAlignment' property of a IntegerField to the integer value displayed in the field.

ComponentValue<Integer, NumberField<Integer>> fieldValue =
        Components.integerField()
                .buildComponentValue();

NumberField<Integer> horizontalAlignmentField = fieldValue.component();

Value<Integer> horizontalAlignmentValue =
        Value.propertyValue(horizontalAlignmentField, "horizontalAlignment",
                int.class, Utilities.propertyChangeObserver(horizontalAlignmentField, "horizontalAlignment"));

fieldValue.link(horizontalAlignmentValue);

JPanel panel = new JPanel();
panel.add(horizontalAlignmentField);

fieldValue.addListener(panel::revalidate);

Dialogs.componentDialog(panel)
        .title("test")
        .show();

2.3.3. Input Components Demo

Application Model
Show code
import static is.codion.common.value.Value.value;

public final class ApplicationModel {

  private final Value<String> shortStringValue = value();
  private final Value<String> longStringValue = value();
  private final Value<String> textValue = value();
  private final Value<LocalDate> localDateValue = value();
  private final Value<LocalDateTime> localDateTimeValue = value();
  private final Value<String> formattedStringValue = value();
  private final Value<Integer> integerValue = value();
  private final Value<Double> doubleValue = value();
  private final Value<Boolean> booleanValue = value();
  private final Value<Boolean> booleanSelectionValue = value();
  private final Value<Integer> integerItemValue = value();
  private final Value<String> stringSelectionValue = value();
  private final Value<Integer> integerSlideValue = value();
  private final Value<Integer> integerSpinValue = value();
  private final Value<Integer> integerSelectionValue = value();
  private final Value<String> itemSpinValue = value();
  private final Value<String> stringListValue = value();
  private final Value<String> messageValue = value();

  private final Collection<Value<?>> values = asList(
          shortStringValue,
          longStringValue,
          textValue,
          localDateValue,
          localDateTimeValue,
          formattedStringValue,
          integerValue,
          doubleValue,
          booleanValue,
          booleanSelectionValue,
          integerItemValue,
          stringSelectionValue,
          integerSlideValue,
          integerSpinValue,
          integerSelectionValue,
          itemSpinValue,
          stringListValue
  );

  public ApplicationModel() {
    setDefaultUncaughtExceptionHandler(this::exceptionHandler);
    values.forEach(value -> value.addDataListener(this::setMessage));
  }

  public void clear() {
    values.forEach(value -> value.set(null));
  }

  public Value<String> shortStringValue() {
    return shortStringValue;
  }

  public Value<String> longStringValue() {
    return longStringValue;
  }

  public Value<String> textValue() {
    return textValue;
  }

  public Value<LocalDate> localDateValue() {
    return localDateValue;
  }

  public Value<LocalDateTime> localDateTimeValue() {
    return localDateTimeValue;
  }

  public Value<Integer> integerValue() {
    return integerValue;
  }

  public Value<Double> doubleValue() {
    return doubleValue;
  }

  public Value<String> formattedStringValue() {
    return formattedStringValue;
  }

  public Value<Boolean> booleanValue() {
    return booleanValue;
  }

  public Value<Boolean> booleanSelectionValue() {
    return booleanSelectionValue;
  }

  public Value<Integer> integerItemValue() {
    return integerItemValue;
  }

  public Value<Integer> integerSlideValue() {
    return integerSlideValue;
  }

  public Value<Integer> integerSpinValue() {
    return integerSpinValue;
  }

  public Value<Integer> integerSelectionValue() {
    return integerSelectionValue;
  }

  public Value<String> itemSpinnerValue() {
    return itemSpinValue;
  }

  public Value<String> stringSelectionValue() {
    return stringSelectionValue;
  }

  public Value<String> stringListValue() {
    return stringListValue;
  }

  public ValueObserver<String> messageObserver() {
    return messageValue.observer();
  }

  private void exceptionHandler(Thread thread, Throwable exception) {
    messageValue.set(exception.getMessage());
  }

  private <T> void setMessage(T value) {
    messageValue.set(value == null ? " " : value.toString());
  }
}
Application Panel
Show code
import static is.codion.swing.common.ui.component.Components.*;

public final class ApplicationPanel extends JPanel {

  public ApplicationPanel(ApplicationModel model) {
    super(borderLayout());

    setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

    JPanel settingsPanel = new JPanel(borderLayout());

    State inputEnabledState = State.state(true);

    checkBox(inputEnabledState)
            .caption("Enabled")
            .mnemonic('N')
            .transferFocusOnEnter(true)
            .build(checkBox -> settingsPanel.add(checkBox, BorderLayout.WEST));

    button(Control.builder(model::clear)
            .enabledState(inputEnabledState)
            .build())
            .caption("Clear")
            .mnemonic('L')
            .transferFocusOnEnter(true)
            .build(button -> settingsPanel.add(button, BorderLayout.EAST));

    JPanel inputPanel = new JPanel(Layouts.flexibleGridLayout(0, 2));

    textField(model.shortStringValue())
            .columns(20)
            .lowerCase(true)
            .maximumLength(20)
            .selectAllOnFocusGained(true)
            .transferFocusOnEnter(true)
            .validator(new PGValidator())
            .selectionProvider(Dialogs.selectionProvider(() ->
                    Arrays.asList("a", "few", "short", "strings", "to", "choose", "from")))
            .label(label("Short String (1)")
                    .displayedMnemonic('1')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    textInputPanel(model.longStringValue())
            .columns(20)
            .maximumLength(400)
            .buttonFocusable(true)
            .selectAllOnFocusGained(true)
            .transferFocusOnEnter(true)
            .label(label("Long String (2)")
                    .displayedMnemonic('2')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    textArea(model.textValue())
            .rowsColumns(4, 20)
            .lineWrap(true)
            .wrapStyleWord(true)
            .transferFocusOnEnter(true)
            .dragEnabled(true)
            .transferHandler(new FilePathTransferHandler())
            .keyEvent(KeyEvents.builder(KeyEvent.VK_SPACE)
                    .modifiers(KeyEvent.CTRL_DOWN_MASK)
                    .action(Control.actionControl(actionEvent ->
                            ((JTextArea) actionEvent.getSource()).append("SPACE"))))
            .label(label("Text (3)")
                    .displayedMnemonic('3')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .scrollPane()
            .build(inputPanel::add);

    maskedTextField(model.formattedStringValue())
            .mask("(##) ##-##")
            .placeholderCharacter('_')
            .placeholder("(00) 00-00")
            .emptyStringToNullValue(true)
            .invalidStringToNullValue(true)
            .valueContainsLiteralCharacters(true)
            .commitsOnValidEdit(true)
            .focusLostBehaviour(JFormattedTextField.COMMIT)
            .transferFocusOnEnter(true)
            .label(label("Formatted String (4)")
                    .displayedMnemonic('4')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    comboBox(createStringComboBoxModel(), model.stringSelectionValue())
            .editable(true)
            .mouseWheelScrolling(true)
            .transferFocusOnEnter(true)
            .label(label("String Selection (5)")
                    .displayedMnemonic('5')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    localDateField(LocaleDateTimePattern.builder()
            .delimiterDash()
            .yearFourDigits()
            .build()
            .dateTimePattern(), model.localDateValue())
            .transferFocusOnEnter(true)
            .label(label("Date (6)")
                    .displayedMnemonic('6')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    localDateTimeInputPanel(LocaleDateTimePattern.builder()
            .delimiterDot()
            .yearTwoDigits()
            .hoursMinutes()
            .build()
            .dateTimePattern(), model.localDateTimeValue())
            .transferFocusOnEnter(true)
            .label(label("Date Time (7)")
                    .displayedMnemonic('7')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    integerField(model.integerValue())
            .valueRange(0, 10_000)
            .groupingUsed(true)
            .groupingSeparator('.')
            .transferFocusOnEnter(true)
            .label(label("Integer (8)")
                    .displayedMnemonic('8')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    doubleField(model.doubleValue())
            .nullable(false)
            .valueRange(0, 1_000_000)
            .groupingUsed(true)
            .maximumFractionDigits(2)
            .decimalSeparator(',')
            .groupingSeparator('.')
            .transferFocusOnEnter(true)
            .label(label("Double (9)")
                    .displayedMnemonic('9')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    ItemComboBoxModel<Integer> integerItemComboBoxModel = createIntegerItemComboBoxModel();
    Value<Integer> integerItemSelectorValue = integerItemComboBoxModel.createSelectorValue(new IntegerItemFinder());
    NumberField<Integer> integerItemSelectorField = integerField(integerItemSelectorValue)
            .columns(2)
            .horizontalAlignment(SwingConstants.CENTER)
            .selectAllOnFocusGained(true)
            .transferFocusOnEnter(true)
            .label(label("Integer Item (A)")
                    .displayedMnemonic('A')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build();
    JComboBox<Item<Integer>> integerItemComboBox = itemComboBox(integerItemComboBoxModel, model.integerItemValue())
            .completionMode(Completion.Mode.AUTOCOMPLETE)
            .popupMenuControl(createSelectRandomItemControl(integerItemComboBoxModel))
            .mouseWheelScrollingWithWrapAround(true)
            .transferFocusOnEnter(true)
            .enabledState(inputEnabledState)
            .build();
    panel(borderLayout())
            .add(integerItemSelectorField, BorderLayout.WEST)
            .add(integerItemComboBox, BorderLayout.CENTER)
            .build(inputPanel::add);

    slider(createSliderModel(), model.integerSlideValue())
            .paintTicks(true)
            .paintTrack(true)
            .minorTickSpacing(5)
            .majorTickSpacing(20)
            .mouseWheelScrolling(true)
            .transferFocusOnEnter(true)
            .label(label("Integer Slide (B)")
                    .displayedMnemonic('B')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    integerSpinner(createSpinnerModel(), model.integerSpinValue())
            .columns(4)
            .mouseWheelScrolling(true)
            .transferFocusOnEnter(true)
            .label(label("Integer Spin (C)")
                    .displayedMnemonic('C')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    comboBox(createIntegerComboBoxModel(), model.integerSelectionValue())
            .editable(true)
            .mouseWheelScrolling(true)
            .transferFocusOnEnter(true)
            .label(label("Integer Selection (D)")
                    .displayedMnemonic('D')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    itemSpinner(createItemSpinnerModel(), model.itemSpinnerValue())
            .columns(20)
            .horizontalAlignment(SwingConstants.CENTER)
            .mouseWheelScrolling(true)
            .transferFocusOnEnter(true)
            .label(label("Item Spin (E)")
                    .displayedMnemonic('E')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    checkBox(model.booleanValue())
            .horizontalAlignment(SwingConstants.CENTER)
            .transferFocusOnEnter(true)
            .label(label("Boolean (F)")
                    .displayedMnemonic('F')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    booleanComboBox(model.booleanSelectionValue())
            .mouseWheelScrolling(true)
            .transferFocusOnEnter(true)
            .enabledState(inputEnabledState)
            .label(label("Boolean Selection (G)")
                    .displayedMnemonic('G')
                    .build(inputPanel::add))
            .build(inputPanel::add);

    Components.list(createStringListModel(), model.stringListValue())
            .visibleRowCount(4)
            .selectionMode(ListSelectionModel.SINGLE_SELECTION)
            .layoutOrientation(JList.HORIZONTAL_WRAP)
            .transferFocusOnEnter(true)
            .label(label("Text List Selection (H)")
                    .displayedMnemonic('H')
                    .build(inputPanel::add))
            .enabledState(inputEnabledState)
            .build(inputPanel::add);

    add(settingsPanel, BorderLayout.NORTH);
    add(inputPanel, BorderLayout.CENTER);

    textField()
            .columns(20)
            .editable(false)
            .focusable(false)
            .border(BorderFactory.createTitledBorder("Message"))
            .enabledState(inputEnabledState)
            .linkedValueObserver(model.messageObserver())
            .build(component -> add(component, BorderLayout.SOUTH));

    Sizes.setPreferredWidth(this, 400);
  }

  private static class PGValidator implements Value.Validator<String> {

    private final List<String> swearWords = asList("fuck", "shit");

    @Override
    public void validate(String value) throws IllegalArgumentException {
      if (value != null) {
        String lowerCaseValue = value.toLowerCase();
        swearWords.forEach(swearWord -> {
          if (lowerCaseValue.contains(swearWord)) {
            throw new IllegalArgumentException("No swearing please");
          }
        });
      }
    }
  }

  private static class FilePathTransferHandler extends TransferHandler {

    @Override
    public boolean canImport(TransferSupport support) {
      return Arrays.stream(support.getDataFlavors())
              .anyMatch(DataFlavor::isFlavorJavaFileListType);
    }

    @Override
    public boolean importData(TransferSupport support) {
      try {
        ((JTextArea) support.getComponent()).setText(support.getTransferable()
                .getTransferData(DataFlavor.javaFileListFlavor).toString());

        return true;
      }
      catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static SpinnerNumberModel createSpinnerModel() {
    return new SpinnerNumberModel(0, 0, 100, 10);
  }

  private static SpinnerListModel createItemSpinnerModel() {
    return new SpinnerListModel(Arrays.asList(
            Item.item("Hello"),
            Item.item("Everybody"),
            Item.item("How"),
            Item.item("Are"),
            Item.item("You")
    ));
  }

  private static DefaultBoundedRangeModel createSliderModel() {
    return new DefaultBoundedRangeModel(0, 0, 0, 100);
  }

  private static ComboBoxModel<String> createStringComboBoxModel() {
    return new DefaultComboBoxModel<>(new String[] {"Hello", "Everybody", "How", "Are", "You"});
  }

  private static ListModel<String> createStringListModel() {
    DefaultListModel<String> listModel = new DefaultListModel<>();
    listModel.addElement("Here");
    listModel.addElement("Are");
    listModel.addElement("A");
    listModel.addElement("Few");
    listModel.addElement("Elements");
    listModel.addElement("To");
    listModel.addElement("Select");
    listModel.addElement("From");

    return listModel;
  }

  private static ItemComboBoxModel<Integer> createIntegerItemComboBoxModel() {
    return ItemComboBoxModel.createModel(asList(
            Item.item(1, "One"), Item.item(2, "Two"), Item.item(3, "Three"),
            Item.item(4, "Four"), Item.item(5, "Five"), Item.item(6, "Six"),
            Item.item(7, "Seven"), Item.item(8, "Eight"), Item.item(9, "Nine")
    ));
  }

  private static final class IntegerItemFinder implements ItemFinder<Item<Integer>, Integer> {

    @Override
    public Integer value(Item<Integer> item) {
      return item.value();
    }

    @Override
    public Predicate<Item<Integer>> createPredicate(Integer value) {
      return item -> Objects.equals(item.value(), value);
    }
  }

  private static ComboBoxModel<Integer> createIntegerComboBoxModel() {
    return new DefaultComboBoxModel<>(new Integer[] {101, 202, 303, 404});
  }

  private static Control createSelectRandomItemControl(ItemComboBoxModel<Integer> integerItemComboBoxModel) {
    Random random = new Random();
    return Control.builder(() ->
                    integerItemComboBoxModel.setSelectedItem(random.nextInt(integerItemComboBoxModel.getSize()) + 1))
            .caption("Select Random Item")
            .build();
  }

  public static void main(String[] args) {
    ApplicationModel applicationModel = new ApplicationModel();

    ApplicationPanel applicationPanel = new ApplicationPanel(applicationModel);

    Dialogs.componentDialog(applicationPanel)
            .title("Codion Input Components Demo")
            .icon(Logos.logoTransparent())
            .show();
  }
}

2.4. Common Utilities

Codion provides a few classes with miscellanous utility functions.

2.4.1. Model