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

The domain model is based around three classes; Entity, which represents a row in a table, EntityDefinition which contains the meta-data for each entity type (properties, data types and such) and Property, which represents columns. Domain models must extend the Domain class, which serves as a container for the domain entity definitions and as a factory for Entity and Entity.Key instances.

Entities

The Entity class is a map like structure, which maps column values to the property representing each column. Each table is represented by a entity type, identified by a entityId, a String constant unique within the domain.

Note
Typically the entityId constant contains the underlying table name, but you can use whatever identifying string you want and specify the table name via a tableName parameter when defining the entity.
Properties

Each column in a table 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 identified by a propertyId, a String, unique within the Entity.

Note
Typically the propertyId contains the underlying column name, but as with the entityId you can use whatever value you want and specify the column name via setColumnName() when instantiating the Property.
Example

String constants for the Entities and Properties in the World demo, here the entityIds have a T_ prefix (for Type or Table) whereas the propertyIds are prefixed by their respective table names.

public final class World extends Domain {

  public static final String T_CITY = "world.city";
  public static final String CITY_ID = "id";
  public static final String CITY_NAME = "name";
  public static final String CITY_COUNTRY_CODE = "countrycode";
  public static final String CITY_COUNTRY_FK = "country_fk";
  public static final String CITY_DISTRICT = "district";
  public static final String CITY_POPULATION = "population";

  public static final String T_COUNTRY = "world.country";
  public static final String COUNTRY_CODE = "code";
  public static final String COUNTRY_NAME = "name";
  public static final String COUNTRY_CONTINENT = "continent";
  public static final String COUNTRY_REGION = "region";
  public static final String COUNTRY_SURFACEAREA = "surfacearea";
  public static final String COUNTRY_INDEPYEAR = "indepyear";
  public static final String COUNTRY_POPULATION = "population";
  public static final String COUNTRY_LIFEEXPECTANCY = "lifeexpectancy";
  public static final String COUNTRY_GNP = "gnp";
  public static final String COUNTRY_GNPOLD = "gnpold";
  public static final String COUNTRY_LOCALNAME = "localname";
  public static final String COUNTRY_GOVERNMENTFORM = "governmentform";
  public static final String COUNTRY_HEADOFSTATE = "headofstate";
  public static final String COUNTRY_CAPITAL = "capital";
  public static final String COUNTRY_CAPITAL_FK = "capital_fk";
  public static final String COUNTRY_CODE2 = "code2";
  public static final String COUNTRY_CAPITAL_POPULATION = "capital_population";
  public static final String COUNTRY_NO_OF_CITIES = "no_of_cities";
  public static final String COUNTRY_NO_OF_LANGUAGES = "no_of_languages";
  public static final String COUNTRY_FLAG = "flag";

  public static final String T_COUNTRYLANGUAGE = "world.countrylanguage";
  public static final String COUNTRYLANGUAGE_COUNTRY_CODE = "countrycode";
  public static final String COUNTRYLANGUAGE_COUNTRY_FK = "country_fk";
  public static final String COUNTRYLANGUAGE_LANGUAGE = "language";
  public static final String COUNTRYLANGUAGE_ISOFFICIAL = "isofficial";
  public static final String COUNTRYLANGUAGE_PERCENTAGE = "percentage";
  public static final String COUNTRYLANGUAGE_NO_OF_SPEAKERS = "no_of_speakers";
Supported data types

The following data types are supported:

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

  • String (java.sql.Types.VARCHAR)

  • Boolean (java.sql.Types.BOOLEAN)

  • Character (java.sql.Types.CHAR)

  • Blob (java.sql.Types.BLOB)

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 key, although that is of course preferable. The framework does not enforce uniqueness for these properties, so a unique or primary key on the corresponding table columns is strongly recommended.

If no primary key properties are specified the entity is somewhat restricted, it can not be referenced via a foreign key and equals() does 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, Types.VARCHAR, "Country code")
        .updatable(true)
        .maximumLength(3),

In case of composite primary keys you create a ColumnProperty and specify the primary key index. Note that in the example below the column with primary key index 0 is a foreign key column and wrapped in a foreignKeyProperty(), which are discussed a bit later.

foreignKeyProperty(COUNTRYLANGUAGE_COUNTRY_FK, "Country", T_COUNTRY,
        columnProperty(COUNTRYLANGUAGE_COUNTRY_CODE, Types.VARCHAR)
                .primaryKeyIndex(0)
                .updatable(true))
        .nullable(false),
columnProperty(COUNTRYLANGUAGE_LANGUAGE, Types.VARCHAR, "Language")
        .primaryKeyIndex(1)
        .updatable(true),
ColumnProperty

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

columnProperty(COUNTRY_REGION, Types.VARCHAR, "Region")
        .nullable(false)
        .maximumLength(26),
columnProperty(COUNTRY_SURFACEAREA, Types.DOUBLE, "Surface area")
        .nullable(false)
        .useNumberFormatGrouping(true)
        .maximumFractionDigits(2),
columnProperty(COUNTRY_INDEPYEAR, Types.INTEGER, "Indep. year")
        .minimumValue(-2000).maximumValue(2500),
columnProperty(COUNTRY_POPULATION, Types.INTEGER, "Population")
        .nullable(false)
        .useNumberFormatGrouping(true),
columnProperty(COUNTRY_LIFEEXPECTANCY, Types.DOUBLE, "Life expectancy")
        .maximumFractionDigits(1)
        .minimumValue(0).maximumValue(99),
BlobProperty

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

blobProperty(COUNTRY_FLAG, "Flag")
        .eagerlyLoaded(true),
ForeignKeyProperty

ForeignKeyProperty is a wrapper property used to represent a foreign key relation. These foreign keys refer to the primary key of the referenced entity and must be constructed accordingly in case of composite primary keys.

foreignKeyProperty(COUNTRY_CAPITAL_FK, "Capital", T_CITY,
        columnProperty(COUNTRY_CAPITAL)),

Referring to an entity with composite primary key.

Note
The order of the properties must be the same as the order of the respective primary key properties in the referenced entity.
foreignKeyProperty(MASTER_FK, "Master", T_MASTER,
        asList(
                columnProperty(MASTER_ID_1),
                columnProperty(MASTER_ID_2)
        ));

The above assumes the below master definition.

define(T_MASTER,
    columnProperty(ID_1).primaryKeyIndex(0),
    columnProperty(ID_2).primaryKeyIndex(1)
    ...
);
Boolean Properties

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

columnProperty(COUNTRYLANGUAGE_ISOFFICIAL, Types.BOOLEAN, "Is official")
        .columnHasDefaultValue(true)
        .nullable(false),

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

booleanProperty(CUSTOMER_IS_ACTIVE, Types.INTEGER, "Is active", 1, 0)

For boolean columns using unconventional types you can specify the true and false values.

booleanProperty(CUSTOMER_IS_ACTIVE, Types.VARCHAR, "Is active", "true", "false")
booleanProperty(CUSTOMER_IS_ACTIVE, Types.CHAR, "Is active", '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.getBoolean(CUSTOMER_IS_ACTIVE);
DenormalizedViewProperty

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

denormalizedViewProperty(COUNTRY_CAPITAL_POPULATION, COUNTRY_CAPITAL_FK,
        getDefinition(T_CITY).getProperty(CITY_POPULATION), "Capital pop.")
        .useNumberFormatGrouping(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 column in the referenced table when the corresponding reference property value is set.

denormalizedProperty(CUSTOMER_CITY, CUSTOMER_ADDRESS_FK,
        getProperty(T_ADDRESS, ADDRESS_CITY), "City")
Note
The property is not kept in sync if the value of the denormalized property is modified in the referenced entity.
Domain domain = getDomain();
Entity address = domain.entity(T_ADDRESS);
address.put(ADDRESS_CITY, "Syracuse");

Entity customer = domain.entity(T_CUSTOMER);
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"
SubqueryProperty

SubqueryProperty is used to represent a property which gets its value from a subquery returning a single value. Note that in the example below reference_id must be available when the query is run, that is, the entity must include that column as a property.

subqueryProperty(COUNTRY_NO_OF_CITIES, Types.INTEGER, "No. of cities",
        "select count(*) from world.city where countrycode = code"),
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, Types.INTEGER, "No. of speakers",
        new NoOfSpeakersProvider(), COUNTRYLANGUAGE_COUNTRY_FK, COUNTRYLANGUAGE_PERCENTAGE)
        .useNumberFormatGrouping(true)
private static final class NoOfSpeakersProvider implements DerivedProperty.Provider {

  private static final long serialVersionUID = 1;

  @Override
  public Object getValue(Map<String, Object> sourceValues) {
    Double percentage = (Double) sourceValues.get(COUNTRYLANGUAGE_PERCENTAGE);
    Entity country = (Entity) sourceValues.get(COUNTRYLANGUAGE_COUNTRY_FK);
    if (notNull(percentage, country) && country.isNotNull(COUNTRY_POPULATION)) {
      return Double.valueOf(country.getInteger(COUNTRY_POPULATION) * (percentage / 100)).intValue();
    }

    return null;
  }
}
Domain

Each entity type is defined by calling Domain.define. The framework assumes the entityId is the table name, unless the tableName parameter is specified.

void city() {
  define(T_CITY,
          primaryKeyProperty(CITY_ID),
          columnProperty(CITY_NAME, Types.VARCHAR, "Name")
                  .nullable(false)
                  .maximumLength(35),
          foreignKeyProperty(CITY_COUNTRY_FK, "Country", T_COUNTRY,
                  columnProperty(CITY_COUNTRY_CODE, Types.VARCHAR))
                  .nullable(false),
          columnProperty(CITY_DISTRICT, Types.VARCHAR, "District")
                  .nullable(false)
                  .maximumLength(20),
          columnProperty(CITY_POPULATION, Types.INTEGER, "Population")
                  .nullable(false)
                  .useNumberFormatGrouping(true))
          .keyGenerator(sequence("world.city_seq"))
          .validator(new CityValidator())
          .orderBy(orderBy().ascending(CITY_NAME))
          .searchPropertyIds(CITY_NAME)
          .stringProvider(new StringProvider(CITY_NAME))
          .colorProvider(new CityColorProvider())
          .caption("City");
}
KeyGenerator

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

Auto-increment

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(T_EMPLOYEE, EMPLOYEE_ID))
Custom

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

private static final class UUIDKeyGenerator implements KeyGenerator {

  @Override
  public void beforeInsert(final Entity entity, final EntityDefinition definition,
                           final DatabaseConnection connection)
          throws SQLException {
    entity.put(CUSTOMER_ID, UUID.randomUUID().toString());
  }
}
StringProvider

The StringProvider class is for providing toString() implementations for entities. This value is for example used when entities are displayed in a ComboBox or as a foreign key values in table views.

define(T_ADDRESS,
        primaryKeyProperty(ADDRESS_ID, Types.INTEGER),
        columnProperty(ADDRESS_STREET, Types.VARCHAR, "Street")
                .nullable(false).maximumLength(120),
        columnProperty(ADDRESS_CITY, Types.VARCHAR, "City")
                .nullable(false).maximumLength(50),
        columnProperty(ADDRESS_VALID, Types.BOOLEAN, "Valid")
                .columnHasDefaultValue(true).nullable(false))
        .stringProvider(new StringProvider(ADDRESS_STREET)
                .addText(", ").addValue(ADDRESS_CITY))
        .keyGenerator(automatic(T_ADDRESS))
        .smallDataset(true)
        .caption("Address");

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

private static final class CustomerToString implements Function<Entity, String> {

  @Override
  public String apply(final Entity customer) {
    StringBuilder builder =
            new StringBuilder(customer.getString(CUSTOMER_LAST_NAME))
                    .append(", ")
                    .append(customer.getString(CUSTOMER_FIRST_NAME));
    if (customer.isNotNull(CUSTOMER_EMAIL)) {
      builder.append(" <")
              .append(customer.getString(CUSTOMER_EMAIL))
              .append(">");
    }

    return builder.toString();
  }
}
ColorProvider

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

private static final class CityColorProvider implements ColorProvider {

  private static final long serialVersionUID = 1;

  @Override
  public Object getColor(Entity city, Property property) {
    if (property.is(CITY_POPULATION) &&
            city.getInteger(CITY_POPULATION) > 1_000_000) {
      //population YELLOW if > 1.000.000
      return Color.YELLOW;
    }
    if (property.is(CITY_NAME) &&
            Objects.equals(city.get(World.CITY_ID),
                    city.getForeignKey(World.CITY_COUNTRY_FK).get(World.COUNTRY_CAPITAL))) {
      //name CYAN if capital city
      return Color.CYAN;
    }

    return null;
  }
}
Validation

Custom validation of Entities is performed by implementing a Entity.Validator.

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.

private static final class CityValidator extends DefaultEntityValidator {

  private static final long serialVersionUID = 1;

  @Override
  public void validate(Entity city, EntityDefinition cityDefinition) throws ValidationException {
    super.validate(city, cityDefinition);
    //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.getForeignKey(CITY_COUNTRY_FK);
    Integer cityPopulation = city.getInteger(CITY_POPULATION);
    Integer countryPopulation = country.getInteger(COUNTRY_POPULATION);
    if (countryPopulation != null && cityPopulation > countryPopulation) {
      throw new ValidationException(CITY_POPULATION,
              cityPopulation, "City population can not exceed country population");
    }
  }
}
Entities in action

Using the Entity class is rather straight forward.

EntityConnectionProvider connectionProvider = EntityConnectionProviders.connectionProvider()
        .setDomainClassName(Petstore.class.getName())
        .setClientTypeId("Manual")
        .setUser(Users.parseUser("scott:tiger"));

Domain store = connectionProvider.getDomain();

EntityConnection connection = connectionProvider.getConnection();

//populate a new category
Entity insects = store.entity(T_CATEGORY);
insects.put(CATEGORY_NAME, "Insects");
insects.put(CATEGORY_DESCRIPTION, "Creepy crawlies");

connection.insert(insects);

//populate a new product for the insect category
Entity smallBeetles = store.entity(T_PRODUCT);
smallBeetles.put(PRODUCT_CATEGORY_FK, insects);
smallBeetles.put(PRODUCT_NAME, "Small Beetles");
smallBeetles.put(PRODUCT_DESCRIPTION, "Beetles on the smaller side");

connection.insert(smallBeetles);

//see what products are available for the Cats category
Entity categoryCats = connection.selectSingle(T_CATEGORY, CATEGORY_NAME, "Cats");

List<Entity> catProducts = connection.select(T_PRODUCT, 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.

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

  • initializeTestEntity should return a 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(String entityId) method with the id of the given entity as parameter. You can either create a single testDomain() method and call the test method in turn for each entityId 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(Store.T_CUSTOMER);
  }

  @Test
  public void address() throws Exception {
    test(Store.T_ADDRESS);
  }

  @Test
  public void customerAddress() throws Exception {
    test(Store.T_CUSTOMER_ADDRESS);
  }

  @Override
  protected Entity initializeReferenceEntity(String entityId,
                                             Map<String, Entity> foreignKeyEntities) {
    //see if the currently running test requires an ADDRESS entity
    if (entityId.equals(Store.T_ADDRESS)) {
      Entity address = getDomain().entity(Store.T_ADDRESS);
      address.put(Store.ADDRESS_ID, 21);
      address.put(Store.ADDRESS_STREET, "One Way");
      address.put(Store.ADDRESS_CITY, "Sin City");

      return address;
    }

    return super.initializeReferenceEntity(entityId, foreignKeyEntities);
  }

  @Override
  protected Entity initializeTestEntity(String entityId,
                                        Map<String, Entity> foreignKeyEntities) {
    if (entityId.equals(Store.T_ADDRESS)) {
      //Initialize a entity representing the table STORE.ADDRESS,
      //which can be used for the testing
      Entity address = getDomain().entity(Store.T_ADDRESS);
      address.put(Store.ADDRESS_ID, 42);
      address.put(Store.ADDRESS_STREET, "Street");
      address.put(Store.ADDRESS_CITY, "City");

      return address;
    }
    else if (entityId.equals(Store.T_CUSTOMER)) {
      //Initialize a entity representing the table STORE.CUSTOMER,
      //which can be used for the testing
      Entity customer = getDomain().entity(Store.T_CUSTOMER);
      customer.put(Store.CUSTOMER_ID, 42);
      customer.put(Store.CUSTOMER_FIRST_NAME, "Robert");
      customer.put(Store.CUSTOMER_LAST_NAME, "Ford");
      customer.put(Store.CUSTOMER_IS_ACTIVE, true);

      return customer;
    }
    else if (entityId.equals(Store.T_CUSTOMER_ADDRESS)) {
      Entity customerAddress = getDomain().entity(Store.T_CUSTOMER_ADDRESS);
      customerAddress.put(Store.CUSTOMER_ADDRESS_CUSTOMER_FK, foreignKeyEntities.get(Store.T_CUSTOMER));
      customerAddress.put(Store.CUSTOMER_ADDRESS_ADDRESS_FK, foreignKeyEntities.get(Store.T_ADDRESS));

      return customerAddress;
    }

    return super.initializeTestEntity(entityId, foreignKeyEntities);
  }

  @Override
  protected void modifyEntity(Entity testEntity,
                              Map<String, Entity> foreignKeyEntities) {
    if (testEntity.is(Store.T_ADDRESS)) {
      testEntity.put(Store.ADDRESS_STREET, "New Street");
      testEntity.put(Store.ADDRESS_CITY, "New City");
    }
    else if (testEntity.is(Store.T_CUSTOMER)) {
      //It is sufficient to change the value of a single property, but the more the merrier
      testEntity.put(Store.CUSTOMER_FIRST_NAME, "Jesse");
      testEntity.put(Store.CUSTOMER_LAST_NAME, "James");
      testEntity.put(Store.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 element.

Condition.Set

Condition.Set represents a set of Conditions, which are either AND’ed or OR’ed together.

EntityCondition

EntityCondition represents a where clause for a entity type.

EntitySelectCondition

EntitySelectCondition represents a where clause for a entity type specifically for selecting.

EntityUpdateCondition

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

1.1.3. EntityConnection

The JMinor 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
select

For selecting one or more rows.

EntitySelectCondition condition =
        entitySelectCondition(T_ARTIST, ARTIST_NAME, LIKE, "The %");

List<Entity> artists = connection.select(condition);

condition = entitySelectCondition(T_ALBUM, conditionSet(AND,
        propertyCondition(ALBUM_ARTIST_FK, LIKE, artists),
        propertyCondition(ALBUM_TITLE, NOT_LIKE, "%live%")
                .setCaseSensitive(false)));

List<Entity> nonLiveAlbums = connection.select(condition);
Domain domain = connection.getDomain();
Entity.Key key42 = domain.key(T_ARTIST, 42L);
Entity.Key key43 = domain.key(T_ARTIST, 43L);

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

List<Entity> albums = connection.select(T_ALBUM, ALBUM_ARTIST_FK, aliceInChains);
selectSingle

For selecting single rows.

Entity ironMaiden = connection.selectSingle(
        entitySelectCondition(T_ARTIST, ARTIST_NAME, LIKE, "Iron Maiden"));

Entity liveAlbum = connection.selectSingle(
        entitySelectCondition(T_ALBUM, conditionSet(AND,
                propertyCondition(ALBUM_ARTIST_FK, LIKE, ironMaiden),
                propertyCondition(ALBUM_TITLE, LIKE, "%live after%")
                        .setCaseSensitive(false))));
Entity.Key key42 = connection.getDomain().key(T_ARTIST, 42L);

Entity artists = connection.selectSingle(key42);
Entity aliceInChains = connection.selectSingle(T_ARTIST, ARTIST_NAME, "Alice In Chains");

//we only have one album by Alice in Chains
//otherwise this would throw an exception
Entity albumFacelift = connection.selectSingle(T_ALBUM, ALBUM_ARTIST_FK, aliceInChains);
selectValues

For selecting the values of a single column.

List<String> customerUsStates = connection.selectValues(CUSTOMER_STATE,
        entityCondition(T_CUSTOMER, CUSTOMER_COUNTRY, LIKE, "USA"));
selectDependencies

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

List<Entity> employees = connection.select(entitySelectCondition(T_EMPLOYEE));

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

Collection<Entity> customersDependingOnEmployees = dependencies.get(T_CUSTOMER);
selectRowCount

For selecting the row count given a condition.

int numberOfItStaff = connection.selectRowCount(
        entityCondition(T_EMPLOYEE, EMPLOYEE_TITLE, LIKE, "IT Staff"));
Modifying
insert

For inserting rows.

Domain domain = connection.getDomain();

Entity myBand = domain.entity(T_ARTIST);
myBand.put(ARTIST_NAME, "My Band");

connection.insert(myBand);

Entity album = domain.entity(T_ALBUM);
album.put(ALBUM_ARTIST_FK, myBand);
album.put(ALBUM_TITLE, "First album");

connection.insert(album);
update

For updating existing rows.

Entity myBand = connection.selectSingle(T_ARTIST, ARTIST_NAME, "My Band");

myBand.put(ARTIST_NAME, "Proper Name");

connection.update(myBand);
EntityUpdateCondition updateCondition =
        entityUpdateCondition(T_ARTIST, ARTIST_NAME, LIKE, "Azymuth");

updateCondition.set(ARTIST_NAME, "Another Name");

connection.update(updateCondition);
delete

For deleting existing rows.

Entity myBand = connection.selectSingle(T_ARTIST, ARTIST_NAME, "Proper Name");

int deleteCount = connection.delete(entityCondition(T_ALBUM, ALBUM_ARTIST_FK, LIKE, myBand));
Entity myBand = connection.selectSingle(T_ARTIST, ARTIST_NAME, "Proper Name");

boolean deleted = connection.delete(myBand.getKey());
Procedures & Functions
executeProcedure
connection.executeProcedure(P_UPDATE_TOTALS);
executeFunction
List<Long> trackIds = asList(123L, 1234L);
BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

List<Entity> modifiedTracks = connection.executeFunction(F_RAISE_PRICE, trackIds, priceIncrease);
Reporting
fillReport
Map<String, Object> reportParameters = new HashMap<>();
reportParameters.put("CUSTOMER_IDS", asList(42, 43, 45));

JasperReportsWrapper reportsWrapper = new JasperReportsWrapper(
        "build/classes/reports/customer_report.jasper", reportParameters);

JasperReportsResult reportResult =
        (JasperReportsResult) connection.fillReport(reportsWrapper);

JasperPrint jasperPrint = reportResult.getResult();
Full example
Show code
package org.jminor.framework.demos.chinook.manual;

import org.jminor.common.db.Database;
import org.jminor.common.db.Databases;
import org.jminor.common.db.exception.DatabaseException;
import org.jminor.common.db.reports.ReportException;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnection;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.db.condition.EntitySelectCondition;
import org.jminor.framework.db.condition.EntityUpdateCondition;
import org.jminor.framework.db.local.LocalEntityConnectionProvider;
import org.jminor.framework.demos.chinook.domain.impl.ChinookImpl;
import org.jminor.framework.domain.Domain;
import org.jminor.framework.domain.entity.Entity;
import org.jminor.plugin.jasperreports.model.JasperReportsResult;
import org.jminor.plugin.jasperreports.model.JasperReportsWrapper;

import net.sf.jasperreports.engine.JasperPrint;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.util.Arrays.asList;
import static org.jminor.common.Conjunction.AND;
import static org.jminor.common.db.ConditionType.LIKE;
import static org.jminor.common.db.ConditionType.NOT_LIKE;
import static org.jminor.framework.db.condition.Conditions.*;
import static org.jminor.framework.demos.chinook.domain.Chinook.*;

/**
 * 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 selectCondition(EntityConnection connection) throws DatabaseException {
    // tag::selectCondition[]
    EntitySelectCondition condition =
            entitySelectCondition(T_ARTIST, ARTIST_NAME, LIKE, "The %");

    List<Entity> artists = connection.select(condition);

    condition = entitySelectCondition(T_ALBUM, conditionSet(AND,
            propertyCondition(ALBUM_ARTIST_FK, LIKE, artists),
            propertyCondition(ALBUM_TITLE, NOT_LIKE, "%live%")
                    .setCaseSensitive(false)));

    List<Entity> nonLiveAlbums = connection.select(condition);
    // end::selectCondition[]
  }

  static void selectKeys(EntityConnection connection) throws DatabaseException {
    // tag::selectKeys[]
    Domain domain = connection.getDomain();
    Entity.Key key42 = domain.key(T_ARTIST, 42L);
    Entity.Key key43 = domain.key(T_ARTIST, 43L);

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

  static void selectValue(EntityConnection connection) throws DatabaseException {
    // tag::selectValue[]
    List<Entity> aliceInChains = connection.select(T_ARTIST, ARTIST_NAME, "Alice In Chains");

    List<Entity> albums = connection.select(T_ALBUM, ALBUM_ARTIST_FK, aliceInChains);
    // end::selectValue[]
  }

  static void selectSingleCondition(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleCondition[]
    Entity ironMaiden = connection.selectSingle(
            entitySelectCondition(T_ARTIST, ARTIST_NAME, LIKE, "Iron Maiden"));

    Entity liveAlbum = connection.selectSingle(
            entitySelectCondition(T_ALBUM, conditionSet(AND,
                    propertyCondition(ALBUM_ARTIST_FK, LIKE, ironMaiden),
                    propertyCondition(ALBUM_TITLE, LIKE, "%live after%")
                            .setCaseSensitive(false))));
    // end::selectSingleCondition[]
  }

  static void selectSingleKeys(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleKeys[]
    Entity.Key key42 = connection.getDomain().key(T_ARTIST, 42L);

    Entity artists = connection.selectSingle(key42);
    // end::selectSingleKeys[]
  }

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

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

  static void selectValues(EntityConnection connection) throws DatabaseException {
    // tag::selectValues[]
    List<String> customerUsStates = connection.selectValues(CUSTOMER_STATE,
            entityCondition(T_CUSTOMER, CUSTOMER_COUNTRY, LIKE, "USA"));
    // end::selectValues[]
  }

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

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

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

  static void selectRowCount(EntityConnection connection) throws DatabaseException {
    // tag::selectRowCount[]
    int numberOfItStaff = connection.selectRowCount(
            entityCondition(T_EMPLOYEE, EMPLOYEE_TITLE, LIKE, "IT Staff"));
    // end::selectRowCount[]
  }

  static void insert(EntityConnection connection) throws DatabaseException {
    // tag::insert[]
    Domain domain = connection.getDomain();

    Entity myBand = domain.entity(T_ARTIST);
    myBand.put(ARTIST_NAME, "My Band");

    connection.insert(myBand);

    Entity album = domain.entity(T_ALBUM);
    album.put(ALBUM_ARTIST_FK, myBand);
    album.put(ALBUM_TITLE, "First album");

    connection.insert(album);
    // end::insert[]
  }

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

    myBand.put(ARTIST_NAME, "Proper Name");

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

  static void updateCondition(EntityConnection connection) throws DatabaseException {
    // tag::updateCondition[]
    EntityUpdateCondition updateCondition =
            entityUpdateCondition(T_ARTIST, ARTIST_NAME, LIKE, "Azymuth");

    updateCondition.set(ARTIST_NAME, "Another Name");

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

  static void deleteCondition(EntityConnection connection) throws DatabaseException {
    // tag::deleteCondition[]
    Entity myBand = connection.selectSingle(T_ARTIST, ARTIST_NAME, "Proper Name");

    int deleteCount = connection.delete(entityCondition(T_ALBUM, ALBUM_ARTIST_FK, LIKE, myBand));
    // end::deleteCondition[]
  }

  static void deleteKey(EntityConnection connection) throws DatabaseException {
    // tag::deleteKey[]
    Entity myBand = connection.selectSingle(T_ARTIST, ARTIST_NAME, "Proper Name");

    boolean deleted = connection.delete(myBand.getKey());
    // end::deleteKey[]
  }

  static void procedure(EntityConnection connection) throws DatabaseException {
    // tag::procedure[]
    connection.executeProcedure(P_UPDATE_TOTALS);
    // end::procedure[]
  }

  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(F_RAISE_PRICE, trackIds, priceIncrease);
    // 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));

    JasperReportsWrapper reportsWrapper = new JasperReportsWrapper(
            "build/classes/reports/customer_report.jasper", reportParameters);

    JasperReportsResult reportResult =
            (JasperReportsResult) connection.fillReport(reportsWrapper);

    JasperPrint jasperPrint = reportResult.getResult();
    //end::report[]
  }

  static void transaction(EntityConnection connection) throws DatabaseException {
    // tag::transaction[]
    try {
      connection.beginTransaction();

      //perform insert/update/delete

      connection.commitTransaction();
    }
    catch (Exception e) {
      connection.rollbackTransaction();
      throw e;
    }
    // end::transaction[]
  }

  static void main(String[] args) throws DatabaseException, ReportException {
    Database.DATABASE_TYPE.set(Database.Type.H2.toString());
    Database.DATABASE_EMBEDDED_IN_MEMORY.set(true);
    Database.DATABASE_INIT_SCRIPT.set("src/main/sql/create_schema.sql");

    EntityConnectionProvider connectionProvider =
            new LocalEntityConnectionProvider(Databases.getInstance())
                    .setDomainClassName(ChinookImpl.class.getName())
                    .setUser(Users.parseUser("scott:tiger"));

    EntityConnection connection = connectionProvider.getConnection();
    selectCondition(connection);
    selectKeys(connection);
    selectValue(connection);
    selectSingleCondition(connection);
    selectSingleKeys(connection);
    selectSingleValue(connection);
    selectValues(connection);
    selectDependencies(connection);
    selectRowCount(connection);
    insert(connection);
    update(connection);
    updateCondition(connection);
    deleteCondition(connection);
    deleteKey(connection);
    procedure(connection);
    function(connection);
    report(connection);
    transaction(connection);
  }
}
Transaction control
try {
  connection.beginTransaction();

  //perform insert/update/delete

  connection.commitTransaction();
}
catch (Exception e) {
  connection.rollbackTransaction();
  throw 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 a 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 getConnection(). 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 getConnection() throws an exception.

A reference to the EntityConnection instance returned by getConnection() 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 getConnection() to be sure you have a healthy EntityConnection.

LocalEntityConnectionProvider

Provides a connection based on a local JDBC connection.

Database.DATABASE_TYPE.set(Database.Type.H2.toString());
Database.DATABASE_EMBEDDED_IN_MEMORY.set(true);
Database.DATABASE_INIT_SCRIPT.set("src/main/sql/create_schema.sql");

LocalEntityConnectionProvider connectionProvider =
        new LocalEntityConnectionProvider(Databases.getInstance());

connectionProvider.setDomainClassName(ChinookImpl.class.getName());
connectionProvider.setUser(Users.parseUser("scott:tiger"));

LocalEntityConnection entityConnection =
        (LocalEntityConnection) connectionProvider.getConnection();

DatabaseConnection databaseConnection =
        entityConnection.getDatabaseConnection();

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

connectionProvider.disconnect();
RemoteEntityConnectionProvider

Provides a connection based on a remote RMI connection.

RemoteEntityConnectionProvider connectionProvider =
        new RemoteEntityConnectionProvider("localhost", -1, 1099);

connectionProvider.setDomainClassName(ChinookImpl.class.getName());
connectionProvider.setUser(Users.parseUser("scott:tiger"));
connectionProvider.setClientTypeId(EntityConnectionProviderDemo.class.getSimpleName());

EntityConnection entityConnection =
        connectionProvider.getConnection();

Domain domain = entityConnection.getDomain();

Entity track = entityConnection.selectSingle(domain.key(Chinook.T_TRACK, 42L));

connectionProvider.disconnect();
HttpEntityConnectionProvider

Provides a connection based on a remote HTTP connection.

HttpEntityConnectionProvider connectionProvider =
        new HttpEntityConnectionProvider("localhost", 8080, false);

connectionProvider.setDomainClassName(ChinookImpl.class.getName());
connectionProvider.setClientTypeId(EntityConnectionProviderDemo.class.getSimpleName());
connectionProvider.setUser(Users.parseUser("scott:tiger"));

EntityConnection entityConnection = connectionProvider.getConnection();

Domain domain = entityConnection.getDomain();

entityConnection.selectSingle(domain.key(Chinook.T_TRACK, 42L));

connectionProvider.disconnect();

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(Store.T_ADDRESS, connectionProvider);
  }
}
public class CustomerModel extends SwingEntityModel {

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

  private void bindEvents() {
    getTableModel().addRefreshStartedListener(() ->
            System.out.println("Refresh is about to start"));

    getEditModel().addValueListener(Store.CUSTOMER_FIRST_NAME, valueChange ->
            System.out.println("Property " + valueChange.getProperty() +
                    " changed from " + valueChange.getPreviousValue() +
                    " to " + valueChange.getValue()));
  }
}
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(Store.T_CUSTOMER, 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(Store.T_CUSTOMER, 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() {
  getTableModel().addRefreshStartedListener(() ->
          System.out.println("Refresh is about to start"));

  getEditModel().addValueListener(Store.CUSTOMER_FIRST_NAME, valueChange ->
          System.out.println("Property " + valueChange.getProperty() +
                  " changed from " + valueChange.getPreviousValue() +
                  " to " + valueChange.getValue()));
}

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(Store.T_CUSTOMER, connectionProvider);
  }
}
EntityConnectionProvider connectionProvider =
        EntityConnectionProviders.connectionProvider()
                .setDomainClassName(Store.class.getName())
                .setUser(Users.parseUser("scott:tiger"))
                .setClientTypeId("StoreMisc");

CustomerEditModel editModel =
        new CustomerEditModel(connectionProvider);

editModel.put(Store.CUSTOMER_ID, 42);
editModel.put(Store.CUSTOMER_FIRST_NAME, "Björn");
editModel.put(Store.CUSTOMER_LAST_NAME, "Sigurðsson");
editModel.put(Store.CUSTOMER_IS_ACTIVE, true);

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

//modify some property values
editModel.put(Store.CUSTOMER_FIRST_NAME, "John");
editModel.put(Store.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(Store.T_CUSTOMER, connectionProvider);
  }
}
public class CustomerAddressTableModel extends SwingEntityTableModel {

  public CustomerAddressTableModel(final EntityConnectionProvider connectionProvider) {
    super(Store.T_CUSTOMER_ADDRESS, 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, Collections.singletonList(new Scenario()));
  }

  @Override
  protected StoreAppModel initializeApplication() {
    EntityConnectionProvider connectionProvider =
            new RemoteEntityConnectionProvider()
                    .setClientId(UUID.randomUUID())
                    .setUser(getUser())
                    .setDomainClassName(Store.class.getName());

    return new StoreAppModel(connectionProvider);
  }

  private static class Scenario extends
          EntityLoadTestModel.AbstractEntityUsageScenario<StoreAppModel> {

    @Override
    protected void performScenario(StoreAppModel application)
            throws ScenarioException {
      try {
        EntityModel customerModel =
                application.getEntityModel(Store.T_CUSTOMER);
        customerModel.refresh();
        selectRandomRow(customerModel.getTableModel());
      }
      catch (Exception e) {
        throw new ScenarioException(e);
      }
    }
  }
}

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

  @Override
  protected void initializeUI() {
    //the firstName field should receive the focus whenever the panel is initialized
    setInitialFocusProperty(Store.CUSTOMER_FIRST_NAME);

    createTextField(Store.CUSTOMER_FIRST_NAME).setColumns(15);
    createTextField(Store.CUSTOMER_LAST_NAME).setColumns(15);
    createTextField(Store.CUSTOMER_EMAIL).setColumns(15);
    createCheckBox(Store.CUSTOMER_IS_ACTIVE, null, false);

    setLayout(new GridLayout(4,1));
    //the createControlPanel method creates a panel containing the
    //component associated with the property as well as a JLabel with the
    //property caption as defined in the domain model
    addPropertyPanel(Store.CUSTOMER_FIRST_NAME);
    addPropertyPanel(Store.CUSTOMER_LAST_NAME);
    addPropertyPanel(Store.CUSTOMER_EMAIL);
    addPropertyPanel(Store.CUSTOMER_IS_ACTIVE);
  }
}
public class AddressEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(Store.ADDRESS_STREET);

    createTextField(Store.ADDRESS_STREET).setColumns(25);
    createTextField(Store.ADDRESS_CITY).setColumns(25);
    createCheckBox(Store.ADDRESS_VALID, null, false);

    setLayout(new GridLayout(3, 1, 5, 5));
    addPropertyPanel(Store.ADDRESS_STREET);
    addPropertyPanel(Store.ADDRESS_CITY);
    addPropertyPanel(Store.ADDRESS_VALID);
  }
}
public class CustomerAddressEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(Store.CUSTOMER_ADDRESS_ADDRESS_FK);

    EntityComboBox addressComboBox =
            createForeignKeyComboBox(Store.CUSTOMER_ADDRESS_ADDRESS_FK);
    Components.setPreferredWidth(addressComboBox, 200);
    Action newAddressAction = EntityEditPanel.createEditPanelAction(addressComboBox,
            new EntityPanelBuilder(Store.T_ADDRESS)
                    .setEditPanelClass(AddressEditPanel.class));
    JPanel addressPanel = Components.createEastButtonPanel(addressComboBox, newAddressAction, false);

    setLayout(new BorderLayout(5, 5));

    add(createPropertyPanel(Store.CUSTOMER_ADDRESS_ADDRESS_FK, addressPanel));
  }
}
Input controls
Boolean
JCheckBox checkBox = createCheckBox(Domain.BOOLEAN_PROPERTY, null, false);

NullableCheckBox nullableCheckBox = createNullableCheckBox(Domain.BOOLEAN_PROPERTY);

JComboBox comboBox = createBooleanComboBox(Domain.BOOLEAN_PROPERTY);
Foreign key
EntityComboBox comboBox = createForeignKeyComboBox(Domain.FOREIGN_KEY_PROPERTY);

EntityLookupField lookupField = createForeignKeyLookupField(Domain.FOREIGN_KEY_PROPERTY);

//readOnly
JTextField textField = createForeignKeyField(Domain.FOREIGN_KEY_PROPERTY);
Temporal
JTextField textField = createTextField(Domain.LOCAL_DATE_PROPERTY);

TemporalInputPanel inputPanel = createTemporalInputPanel(Domain.LOCAL_DATE_PROPERTY);
Numerical
IntegerField integerField = (IntegerField) createTextField(Domain.INTEGER_PROPERTY);

LongField longField = (LongField) createTextField(Domain.LONG_PROPERTY);

DecimalField doubleField = (DecimalField) createTextField(Domain.DOUBLE_PROPERTY);

DecimalField bigDecimalField = (DecimalField) createTextField(Domain.BIG_DECIMAL_PROPERTY);
Text
JTextField textField = createTextField(Domain.TEXT_PROPERTY);

JTextArea textArea = createTextArea(Domain.LONG_TEXT_PROPERTY, 5, 20);

TextInputPanel inputPanel = createTextInputPanel(Domain.LONG_TEXT_PROPERTY);

JFormattedTextField formattedField = (JFormattedTextField)
        createTextField(Domain.FORMATTED_TEXT_PROPERTY, UpdateOn.KEYSTROKE, "###:###");
Value list
SteppedComboBox comboBox = createValueListComboBox(Domain.VALUE_LIST_PROPERTY);
Panels & labels
JLabel label = createLabel(Domain.TEXT_PROPERTY);

JPanel propertyPanel = createPropertyPanel(Domain.TEXT_PROPERTY);
Custom actions

The action mechanism used throughout the JMinor framework is based on the Control class and its subclasses and the ControlSet class which, as the name suggests, represents a set of controls. There are two static utility classes for creating and presenting controls, Controls and ControlProvider respectively.

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. The table popup menu is based on the ControlSet returned by the getPopupControlSet() method in the EntityTablePanel class which in turn uses the ControlSet returned by the getPrintControls() method in the same class for constructing the print popup submenu. So, to add a custom print action you override the getPrintControls() method and return a ControlSet containing the action.

public class CustomerTablePanel extends EntityTablePanel {

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

  @Override
  protected ControlSet getPrintControls() {
    ControlSet printControls = super.getPrintControls();
    //add a Control which calls the viewCustomerReport method in this class
    //enabled only when the selection is not empty
    printControls.add(Controls.control(this::viewCustomerReport, "Customer report",
            getTable().getModel().getSelectionModel().getSelectionNotEmptyObserver()));

    return printControls;
  }

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

    String reportPath = "http://test.io/customer_report.jasper";
    Collection<Integer> customerIds = Entities.getValues(Store.CUSTOMER_ID, selectedCustomers);
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", customerIds);

    EntityReportUiUtil.viewJdbcReport(this,
            new JasperReportsWrapper(reportPath, reportParameters),
            new JasperReportsUIWrapper(),  "Customer Report",
            getTableModel().getConnectionProvider());
  }
}

1.3.4. EntityPanelBuilder

The EntityPanelBuilder class provides lazy initialization for EntityPanels. Using the EntityPanelBuilder class instead of instantiating EntityPanels directly means the panels are not initialized until made visible.

  @Override
  protected void setupEntityPanelBuilders() {
    final SwingEntityModelBuilder countryModelBuilder = new SwingEntityModelBuilder(World.T_COUNTRY);
    countryModelBuilder.setModelClass(CountryModel.class);
    EntityPanelBuilder countryPanelBuilder = new EntityPanelBuilder(countryModelBuilder);
    countryPanelBuilder.setEditPanelClass(CountryEditPanel.class);
    countryPanelBuilder.setTablePanelClass(CountryTablePanel.class);

    final SwingEntityModelBuilder countryCustomModelBuilder = new SwingEntityModelBuilder(World.T_COUNTRY);
    countryCustomModelBuilder.setModelClass(CountryCustomModel.class);
    EntityPanelBuilder countryCustomPanelBuilder = new EntityPanelBuilder(countryCustomModelBuilder)
            .setPanelClass(CountryCustomPanel.class)
            .setCaption("Custom Country");

    EntityPanelBuilder cityPanelBuilder = new EntityPanelBuilder(World.T_CITY);
    cityPanelBuilder.setEditPanelClass(CityEditPanel.class);

    EntityPanelBuilder countryLanguagePanelBuilder = new EntityPanelBuilder(World.T_COUNTRYLANGUAGE);
    countryLanguagePanelBuilder.setEditPanelClass(CountryLanguageEditPanel.class);

    countryPanelBuilder.addDetailPanelBuilder(cityPanelBuilder);
    countryPanelBuilder.addDetailPanelBuilder(countryLanguagePanelBuilder);

    EntityPanelBuilder continentPanelBuilder = new EntityPanelBuilder(World.T_CONTINENT)
            .setPanelClass(ContinentPanel.class);
    EntityPanelBuilder lookupPanelBuilder = new EntityPanelBuilder(World.T_LOOKUP)
            .setTablePanelClass(LookupTablePanel.class)
            .setRefreshOnInit(false);

    addEntityPanelBuilders(countryPanelBuilder, countryCustomPanelBuilder, continentPanelBuilder, lookupPanelBuilder);
  }

1.3.5. EntityApplicationPanel

public class StoreAppPanel extends EntityApplicationPanel<StoreAppModel> {

  @Override
  protected List<EntityPanel> initializeEntityPanels(final StoreAppModel applicationModel) {
    CustomerModel customerModel =
            (CustomerModel) applicationModel.getEntityModel(Store.T_CUSTOMER);
    //populate model with rows from database
    customerModel.refresh();

    EntityPanel customerPanel = new EntityPanel(customerModel,
            new CustomerEditPanel(customerModel.getEditModel()),
            new CustomerTablePanel(customerModel.getTableModel()));

    CustomerAddressModel customerAddressModel =
            (CustomerAddressModel) customerModel.getDetailModel(Store.T_CUSTOMER_ADDRESS);
    EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
            new CustomerAddressEditPanel(customerAddressModel.getEditModel()));

    customerPanel.addDetailPanel(customerAddressPanel);

    return Collections.singletonList(customerPanel);
  }

  @Override
  protected void setupEntityPanelBuilders() {
    addSupportPanelBuilder(new EntityPanelBuilder(Store.T_ADDRESS)
            .setEditPanelClass(AddressEditPanel.class));
  }

  @Override
  protected StoreAppModel initializeApplicationModel(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);
    EntityPanel.COMPACT_ENTITY_PANEL_LAYOUT.set(true);
    EntityTablePanel.REFERENTIAL_INTEGRITY_ERROR_HANDLING.set(EntityTablePanel.ReferentialIntegrityErrorHandling.DEPENDENCIES);
    ColumnConditionModel.AUTOMATIC_WILDCARD.set(ColumnConditionModel.AutomaticWildcard.POSTFIX);
    ColumnConditionModel.CASE_SENSITIVE.set(false);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("org.jminor.framework.demos.manual.store.domain.Store");
    new StoreAppPanel().startApplication("Store", null, false,
            Windows.getScreenSizeRatio(0.6), Users.parseUser("scott:tiger"));
  }
}

1.3.6. Reporting with JasperReports

JMinor uses a plugin oriented approach to report viewing, and provides an implementation for JasperReports and NextReports.

With the JMinor JasperReports plugin you can either design your report based on a SQL query in which case you use the JasperReportsWrapper 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 JasperReportsEntityDataSource class, which is constructed around an iterator.

JDBC Reports

Using a report based on a SQL query, JasperReportsWrapper and JasperReportsUIWrapper is the simplest way of viewing a report using JMinor, just add a method similar to the one below to a EntityPanel 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(final SwingEntityTableModel tableModel) {
    super(tableModel);
  }

  @Override
  protected ControlSet getPrintControls() {
    ControlSet printControls = super.getPrintControls();
    //add a Control which calls the viewCustomerReport method in this class
    //enabled only when the selection is not empty
    printControls.add(Controls.control(this::viewCustomerReport, "Customer report",
            getTable().getModel().getSelectionModel().getSelectionNotEmptyObserver()));

    return printControls;
  }

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

    String reportPath = "http://test.io/customer_report.jasper";
    Collection<Integer> customerIds = Entities.getValues(Store.CUSTOMER_ID, selectedCustomers);
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", customerIds);

    EntityReportUiUtil.viewJdbcReport(this,
            new JasperReportsWrapper(reportPath, reportParameters),
            new JasperReportsUIWrapper(),  "Customer Report",
            getTableModel().getConnectionProvider());
  }
}
JRDataSource Reports

The JRDataSource implementation provided by the JasperReportsEntityDataSource simply iterates through the iterator received via the constructor and retrieves the field values from the underlying entities. For this to work you must design the report using field names that correspond to the property IDs, 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. If you need to use a field that does not correspond to a property in the underlying entity, i.e. when combining two fields into one you must override the getFieldValue() method and handle that special case there.

@Override
public Object getFieldValue(JRField jrField) {
  if (jrField.getName().equals("name_category_code") {
    Entity currentRecord = getCurrentEntity();

    return currentRecord.getString(Store.ITEM_NAME) + " - "
             + currentRecord.getAsString(Store.ITEM_CATEGORY_CODE);
  }

  return super.getFieldValue(jrField);
}
Examples

2. Common

2.1. Common Model

The model layer in a JMinor application contains the business logic, most of the events and application state.

2.1.1. Common 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 the Events factory class.

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

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

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

// 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 the States factory class.

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

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

// 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 = States.state();

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

Components.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 the Values factory class.

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

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

value.set(2);

IntegerField integerField = new IntegerField();

ComponentValue<Integer, IntegerField> fieldValue =
        NumericalValues.integerValue(integerField);

value.link(fieldValue);

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

2.2. Common UI

2.2.1. Input Controls

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

Boolean
CheckBox
//non-nullable boolean value
boolean initialValue = true;
boolean nullValue = false;

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

JToggleButton.ToggleButtonModel buttonModel =
        new JToggleButton.ToggleButtonModel();

booleanValue.link(BooleanValues.booleanButtonModelValue(buttonModel));

JCheckBox checkBox = new JCheckBox();
checkBox.setModel(buttonModel);
NullableCheckBox
//nullable boolean value
Value<Boolean> booleanValue = Values.value();

NullableToggleButtonModel buttonModel =
        new NullableToggleButtonModel();

booleanValue.link(BooleanValues.booleanButtonModelValue(buttonModel));

NullableCheckBox checkBox = new NullableCheckBox(buttonModel);
ComboBox
Value<Boolean> booleanValue = Values.value();

JComboBox<Item<Boolean>> comboBox = new JComboBox<>(new BooleanComboBoxModel());

booleanValue.link(BooleanValues.booleanComboBoxValue(comboBox));
Text
TextField
Value<String> stringValue = Values.value();

JTextField textField = new JTextField();

stringValue.link(TextValues.textValue(textField));
TextArea
Value<String> stringValue = Values.value();

JTextArea textArea = new JTextArea();

stringValue.link(TextValues.textValue(textArea));
Numbers
Integer
Value<Integer> integerValue = Values.value();

IntegerField integerField = new IntegerField();

integerValue.link(NumericalValues.integerValue(integerField));
Long
Value<Long> longValue = Values.value();

LongField longField = new LongField();

longValue.link(NumericalValues.longValue(longField));
Double
Value<Double> doubleValue = Values.value();

DecimalField doubleField = new DecimalField();

doubleValue.link(NumericalValues.doubleValue(doubleField));
BigDecimal
Value<BigDecimal> bigDecimalValue = Values.value();

DecimalField bigDecimalField = new DecimalField();

bigDecimalValue.link(NumericalValues.bigDecimalValue(bigDecimalField));
Date & Time
LocalTime
Value<LocalTime> localTimeValue = Values.value();

JFormattedTextField textField = new JFormattedTextField();

localTimeValue.link(TemporalValues.localTimeValue(textField, "HH:mm:ss"));
LocalDate
Value<LocalDate> localDateValue = Values.value();

JFormattedTextField textField = new JFormattedTextField();

localDateValue.link(TemporalValues.localDateValue(textField, "dd-MM-yyyy"));
LocalDateTime
Value<LocalDateTime> localDateTimeValue = Values.value();

JFormattedTextField textField = new JFormattedTextField();

localDateTimeValue.link(TemporalValues.localDateTimeValue(textField, "dd-MM-yyyy HH:mm"));
Selection
ComboBox
Value<String> stringValue = Values.value();

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

stringValue.link(SelectedValues.selectedValue(comboBox));
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, 5, 5));
    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(PersonPanel component) {
    return new Person(component.firstNameField.getText(), component.lastNameField.getText());
  }

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

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

PersonPanel personPanel = new PersonPanel();

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

personValue.link(personPanelValue);
Property

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

IntegerField horizontalAlignmentField = new IntegerField(5);

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

Value<Integer> fieldValue =
        NumericalValues.integerValue(horizontalAlignmentField, Nullable.NO);

horizontalAlignmentValue.link(fieldValue);

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

fieldValue.addListener(panel::revalidate);

Dialogs.displayInDialog(null, panel, "test");

2.3. Common Utilities

JMinor provides a few classes with miscellanous utility functions.