1. Domain

package org.jminor.framework.demos.chinook.domain;

import org.jminor.framework.domain.property.DerivedProperty;

import java.math.BigDecimal;

public interface Chinook {

  String T_USER = "user@chinook";
  String USER_USERID = "userid";
  String USER_USERNAME = "username";
  String USER_PASSWORD_HASH = "passwordhash";

  String T_ARTIST = "artist@chinook";
  String ARTIST_ARTISTID = "artistid";
  String ARTIST_NAME = "name";

  String T_ALBUM = "album@chinook";
  String ALBUM_ALBUMID = "albumid";
  String ALBUM_TITLE = "title";
  String ALBUM_ARTISTID = "artistid";
  String ALBUM_ARTIST_FK = "artist_fk";
  String ALBUM_COVER = "cover";
  String ALBUM_COVER_IMAGE = "coverimage";

  String T_EMPLOYEE = "employee@chinook";
  String EMPLOYEE_EMPLOYEEID = "employeeid";
  String EMPLOYEE_LASTNAME = "lastname";
  String EMPLOYEE_FIRSTNAME = "firstname";
  String EMPLOYEE_TITLE = "title";
  String EMPLOYEE_REPORTSTO = "reportsto";
  String EMPLOYEE_REPORTSTO_FK = "reportsto_fk";
  String EMPLOYEE_BIRTHDATE = "birthdate";
  String EMPLOYEE_HIREDATE = "hiredate";
  String EMPLOYEE_ADDRESS = "address";
  String EMPLOYEE_CITY = "city";
  String EMPLOYEE_STATE = "state";
  String EMPLOYEE_COUNTRY = "country";
  String EMPLOYEE_POSTALCODE = "postalcode";
  String EMPLOYEE_PHONE = "phone";
  String EMPLOYEE_FAX = "fax";
  String EMPLOYEE_EMAIL = "email";

  String T_CUSTOMER = "customer@chinook";
  String CUSTOMER_CUSTOMERID = "customerid";
  String CUSTOMER_FIRSTNAME = "firstname";
  String CUSTOMER_LASTNAME = "lastname";
  String CUSTOMER_COMPANY = "company";
  String CUSTOMER_ADDRESS = "address";
  String CUSTOMER_CITY = "city";
  String CUSTOMER_STATE = "state";
  String CUSTOMER_COUNTRY = "country";
  String CUSTOMER_POSTALCODE = "postalcode";
  String CUSTOMER_PHONE = "phone";
  String CUSTOMER_FAX = "fax";
  String CUSTOMER_EMAIL = "email";
  String CUSTOMER_SUPPORTREPID = "supportrepid";
  String CUSTOMER_SUPPORTREP_FK = "supportrep_fk";

  String T_GENRE = "genre@chinook";
  String GENRE_GENREID = "genreid";
  String GENRE_NAME = "name";

  String T_MEDIATYPE = "mediatype@chinook";
  String MEDIATYPE_MEDIATYPEID = "mediatypeid";
  String MEDIATYPE_NAME = "name";

  String T_TRACK = "track@chinook";
  String TRACK_TRACKID = "trackid";
  String TRACK_NAME = "name";
  String TRACK_ARTIST_DENORM = "artist_denorm";
  String TRACK_ALBUMID = "albumid";
  String TRACK_ALBUM_FK = "album_fk";
  String TRACK_MEDIATYPEID = "mediatypeid";
  String TRACK_MEDIATYPE_FK = "mediatype_fk";
  String TRACK_GENREID = "genreid";
  String TRACK_GENRE_FK = "genre_fk";
  String TRACK_COMPOSER = "composer";
  String TRACK_MILLISECONDS = "milliseconds";
  String TRACK_MINUTES_SECONDS_DERIVED = "minutes_seconds_transient";
  String TRACK_BYTES = "bytes";
  String TRACK_UNITPRICE = "unitprice";

  DerivedProperty.Provider TRACK_MIN_SEC_PROVIDER =
          linkedValues -> {
            final Integer milliseconds = (Integer) linkedValues.get(TRACK_MILLISECONDS);
            if (milliseconds == null || milliseconds <= 0) {
              return "";
            }

            final int seconds = ((milliseconds / 1000) % 60);
            final int minutes = ((milliseconds / 1000) / 60);

            return minutes + " min " + seconds + " sec";
          };

  String T_INVOICE = "invoice@chinook";
  String INVOICE_INVOICEID = "invoiceid";
  String INVOICE_INVOICEID_AS_STRING = "invoiceid || ''";
  String INVOICE_CUSTOMERID = "customerid";
  String INVOICE_CUSTOMER_FK = "customer_fk";
  String INVOICE_INVOICEDATE = "invoicedate";
  String INVOICE_BILLINGADDRESS = "billingaddress";
  String INVOICE_BILLINGCITY = "billingcity";
  String INVOICE_BILLINGSTATE = "billingstate";
  String INVOICE_BILLINGCOUNTRY = "billingcountry";
  String INVOICE_BILLINGPOSTALCODE = "billingpostalcode";
  String INVOICE_TOTAL = "total";
  String INVOICE_TOTAL_SUB = "total_sub";

  String T_INVOICELINE = "invoiceline@chinook";
  String INVOICELINE_INVOICELINEID = "invoicelineid";
  String INVOICELINE_INVOICEID = "invoiceid";
  String INVOICELINE_INVOICE_FK = "invoice_fk";
  String INVOICELINE_TRACKID = "trackid";
  String INVOICELINE_TRACK_FK = "track_fk";
  String INVOICELINE_UNITPRICE = "unitprice";
  String INVOICELINE_QUANTITY = "quantity";
  String INVOICELINE_TOTAL = "total";

  DerivedProperty.Provider INVOICELINE_TOTAL_PROVIDER =
          linkedValues -> {
            final Integer quantity = (Integer) linkedValues.get(INVOICELINE_QUANTITY);
            final BigDecimal unitPrice = (BigDecimal) linkedValues.get(INVOICELINE_UNITPRICE);
            if (unitPrice == null || quantity == null) {
              return null;
            }

            return unitPrice.multiply(BigDecimal.valueOf(quantity));
          };

  String T_PLAYLIST = "playlist@chinook";
  String PLAYLIST_PLAYLISTID = "playlistid";
  String PLAYLIST_NAME = "name";

  String T_PLAYLISTTRACK = "playlisttrack@chinook";
  String PLAYLISTTRACK_PLAYLISTID = "playlistid";
  String PLAYLISTTRACK_PLAYLIST_FK = "playlist_fk";
  String PLAYLISTTRACK_TRACKID = "trackid";
  String PLAYLISTTRACK_TRACK_FK = "track_fk";
  String PLAYLISTTRACK_ALBUM_DENORM = "album_denorm";
  String PLAYLISTTRACK_ARTIST_DENORM = "artist_denorm";

  String P_UPDATE_TOTALS = "chinook.update_totals_procedure";
  String F_RAISE_PRICE = "chinook.raise_price_function";
}
package org.jminor.framework.demos.chinook.domain.impl;

import org.jminor.common.db.ConditionType;
import org.jminor.common.db.exception.DatabaseException;
import org.jminor.common.db.operation.AbstractDatabaseFunction;
import org.jminor.common.db.operation.AbstractDatabaseProcedure;
import org.jminor.framework.db.condition.EntitySelectCondition;
import org.jminor.framework.db.local.LocalEntityConnection;
import org.jminor.framework.demos.chinook.domain.Chinook;
import org.jminor.framework.domain.Domain;
import org.jminor.framework.domain.entity.Entity;
import org.jminor.framework.domain.entity.StringProvider;
import org.jminor.framework.domain.property.DerivedProperty;

import javax.imageio.ImageIO;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Types;
import java.text.NumberFormat;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static org.jminor.framework.db.condition.Conditions.entitySelectCondition;
import static org.jminor.framework.domain.entity.Entities.getModifiedEntities;
import static org.jminor.framework.domain.entity.KeyGenerators.automatic;
import static org.jminor.framework.domain.entity.OrderBy.orderBy;
import static org.jminor.framework.domain.property.Properties.*;

public final class ChinookImpl extends Domain implements Chinook {

  public ChinookImpl() {
    user();
    artist();
    album();
    employee();
    customer();
    genre();
    mediaType();
    track();
    invoice();
    invoiceLine();
    playlist();
    playlistTrack();
    dbOperations();
  }

  void user() {
    define(T_USER, "chinook.user",
            primaryKeyProperty(USER_USERID),
            columnProperty(USER_USERNAME, Types.VARCHAR, "Username")
                    .nullable(false)
                    .maximumLength(20),
            columnProperty(USER_PASSWORD_HASH, Types.INTEGER, "Password hash"))
            .keyGenerator(automatic("chinook.user"))
            .orderBy(orderBy().ascending(USER_USERNAME))
            .stringProvider(new StringProvider(USER_USERNAME))
            .searchPropertyIds(USER_USERNAME)
            .caption("Users");
  }

  void artist() {
    define(T_ARTIST, "chinook.artist",
            primaryKeyProperty(ARTIST_ARTISTID, Types.BIGINT),
            columnProperty(ARTIST_NAME, Types.VARCHAR, "Name")
                    .nullable(false)
                    .maximumLength(120)
                    .preferredColumnWidth(160))
            .keyGenerator(automatic("chinook.artist"))
            .orderBy(orderBy().ascending(ARTIST_NAME))
            .stringProvider(new StringProvider(ARTIST_NAME))
            .searchPropertyIds(ARTIST_NAME)
            .caption("Artists");
  }

  void album() {
    define(T_ALBUM, "chinook.album",
            primaryKeyProperty(ALBUM_ALBUMID, Types.BIGINT),
            foreignKeyProperty(ALBUM_ARTIST_FK, "Artist", T_ARTIST,
                    columnProperty(ALBUM_ARTISTID, Types.BIGINT))
                    .nullable(false)
                    .preferredColumnWidth(160),
            columnProperty(ALBUM_TITLE, Types.VARCHAR, "Title")
                    .nullable(false)
                    .maximumLength(160)
                    .preferredColumnWidth(160),
            blobProperty(ALBUM_COVER, "Cover")
                    .eagerlyLoaded(true),
            derivedProperty(ALBUM_COVER_IMAGE, Types.JAVA_OBJECT, null,
                    new CoverArtImageProvider(), ALBUM_COVER))
            .keyGenerator(automatic("chinook.album"))
            .orderBy(orderBy().ascending(ALBUM_ARTISTID, ALBUM_TITLE))
            .stringProvider(new StringProvider(ALBUM_TITLE))
            .searchPropertyIds(ALBUM_TITLE)
            .caption("Albums");
  }

  void employee() {
    define(T_EMPLOYEE, "chinook.employee",
            primaryKeyProperty(EMPLOYEE_EMPLOYEEID, Types.BIGINT),
            columnProperty(EMPLOYEE_LASTNAME, Types.VARCHAR, "Last name")
                    .nullable(false)
                    .maximumLength(20),
            columnProperty(EMPLOYEE_FIRSTNAME, Types.VARCHAR, "First name")
                    .nullable(false)
                    .maximumLength(20),
            columnProperty(EMPLOYEE_TITLE, Types.VARCHAR, "Title")
                    .maximumLength(30),
            foreignKeyProperty(EMPLOYEE_REPORTSTO_FK, "Reports to", T_EMPLOYEE,
                    columnProperty(EMPLOYEE_REPORTSTO, Types.BIGINT)),
            columnProperty(EMPLOYEE_BIRTHDATE, Types.DATE, "Birthdate"),
            columnProperty(EMPLOYEE_HIREDATE, Types.DATE, "Hiredate"),
            columnProperty(EMPLOYEE_ADDRESS, Types.VARCHAR, "Address")
                    .maximumLength(70),
            columnProperty(EMPLOYEE_CITY, Types.VARCHAR, "City")
                    .maximumLength(40),
            columnProperty(EMPLOYEE_STATE, Types.VARCHAR, "State")
                    .maximumLength(40),
            columnProperty(EMPLOYEE_COUNTRY, Types.VARCHAR, "Country")
                    .maximumLength(40),
            columnProperty(EMPLOYEE_POSTALCODE, Types.VARCHAR, "Postal code")
                    .maximumLength(10),
            columnProperty(EMPLOYEE_PHONE, Types.VARCHAR, "Phone")
                    .maximumLength(24),
            columnProperty(EMPLOYEE_FAX, Types.VARCHAR, "Fax")
                    .maximumLength(24),
            columnProperty(EMPLOYEE_EMAIL, Types.VARCHAR, "Email")
                    .maximumLength(60))
            .keyGenerator(automatic("chinook.employee"))
            .orderBy(orderBy().ascending(EMPLOYEE_LASTNAME, EMPLOYEE_FIRSTNAME))
            .stringProvider(new StringProvider(EMPLOYEE_LASTNAME)
                    .addText(", ").addValue(EMPLOYEE_FIRSTNAME))
            .searchPropertyIds(EMPLOYEE_FIRSTNAME, EMPLOYEE_LASTNAME, EMPLOYEE_EMAIL)
            .caption("Employees");
  }

  void customer() {
    define(T_CUSTOMER, "chinook.customer",
            primaryKeyProperty(CUSTOMER_CUSTOMERID, Types.BIGINT),
            columnProperty(CUSTOMER_LASTNAME, Types.VARCHAR, "Last name")
                    .nullable(false)
                    .maximumLength(20),
            columnProperty(CUSTOMER_FIRSTNAME, Types.VARCHAR, "First name")
                    .nullable(false)
                    .maximumLength(40),
            columnProperty(CUSTOMER_COMPANY, Types.VARCHAR, "Company")
                    .maximumLength(80),
            columnProperty(CUSTOMER_ADDRESS, Types.VARCHAR, "Address")
                    .maximumLength(70),
            columnProperty(CUSTOMER_CITY, Types.VARCHAR, "City")
                    .maximumLength(40),
            columnProperty(CUSTOMER_STATE, Types.VARCHAR, "State")
                    .maximumLength(40),
            columnProperty(CUSTOMER_COUNTRY, Types.VARCHAR, "Country")
                    .maximumLength(40),
            columnProperty(CUSTOMER_POSTALCODE, Types.VARCHAR, "Postal code")
                    .maximumLength(10),
            columnProperty(CUSTOMER_PHONE, Types.VARCHAR, "Phone")
                    .maximumLength(24),
            columnProperty(CUSTOMER_FAX, Types.VARCHAR, "Fax")
                    .maximumLength(24),
            columnProperty(CUSTOMER_EMAIL, Types.VARCHAR, "Email")
                    .nullable(false)
                    .maximumLength(60),
            foreignKeyProperty(CUSTOMER_SUPPORTREP_FK, "Support rep", T_EMPLOYEE,
                    columnProperty(CUSTOMER_SUPPORTREPID, Types.BIGINT)))
            .keyGenerator(automatic("chinook.customer"))
            .orderBy(orderBy().ascending(CUSTOMER_LASTNAME, CUSTOMER_FIRSTNAME))
            .stringProvider(new CustomerStringProvider())
            .searchPropertyIds(CUSTOMER_FIRSTNAME, CUSTOMER_LASTNAME, CUSTOMER_EMAIL)
            .caption("Customers");
  }

  void genre() {
    define(T_GENRE, "chinook.genre",
            primaryKeyProperty(GENRE_GENREID, Types.BIGINT),
            columnProperty(GENRE_NAME, Types.VARCHAR, "Name")
                    .nullable(false)
                    .maximumLength(120)
                    .preferredColumnWidth(160))
            .keyGenerator(automatic("chinook.genre"))
            .orderBy(orderBy().ascending(GENRE_NAME))
            .stringProvider(new StringProvider(GENRE_NAME))
            .searchPropertyIds(GENRE_NAME)
            .smallDataset(true)
            .caption("Genres");
  }

  void mediaType() {
    define(T_MEDIATYPE, "chinook.mediatype",
            primaryKeyProperty(MEDIATYPE_MEDIATYPEID, Types.BIGINT),
            columnProperty(MEDIATYPE_NAME, Types.VARCHAR, "Name")
                    .nullable(false)
                    .maximumLength(120)
                    .preferredColumnWidth(160))
            .keyGenerator(automatic("chinook.mediatype"))
            .stringProvider(new StringProvider(MEDIATYPE_NAME))
            .smallDataset(true)
            .caption("Media types");
  }

  void track() {
    define(T_TRACK, "chinook.track",
            primaryKeyProperty(TRACK_TRACKID, Types.BIGINT),
            denormalizedViewProperty(TRACK_ARTIST_DENORM, TRACK_ALBUM_FK,
                    getDefinition(T_ALBUM).getProperty(ALBUM_ARTIST_FK), "Artist")
                    .preferredColumnWidth(160),
            foreignKeyProperty(TRACK_ALBUM_FK, "Album", T_ALBUM,
                    columnProperty(TRACK_ALBUMID, Types.BIGINT))
                    .fetchDepth(2)
                    .preferredColumnWidth(160),
            columnProperty(TRACK_NAME, Types.VARCHAR, "Name")
                    .nullable(false)
                    .maximumLength(200)
                    .preferredColumnWidth(160),
            foreignKeyProperty(TRACK_GENRE_FK, "Genre", T_GENRE,
                    columnProperty(TRACK_GENREID, Types.BIGINT)),
            columnProperty(TRACK_COMPOSER, Types.VARCHAR, "Composer")
                    .maximumLength(220)
                    .preferredColumnWidth(160),
            foreignKeyProperty(TRACK_MEDIATYPE_FK, "Media type", T_MEDIATYPE,
                    columnProperty(TRACK_MEDIATYPEID, Types.BIGINT))
                    .nullable(false),
            columnProperty(TRACK_MILLISECONDS, Types.INTEGER, "Duration (ms)")
                    .nullable(false)
                    .format(NumberFormat.getIntegerInstance()),
            derivedProperty(TRACK_MINUTES_SECONDS_DERIVED, Types.VARCHAR, "Duration (min/sec)",
                    TRACK_MIN_SEC_PROVIDER, TRACK_MILLISECONDS),
            columnProperty(TRACK_BYTES, Types.INTEGER, "Bytes")
                    .format(NumberFormat.getIntegerInstance()),
            columnProperty(TRACK_UNITPRICE, Types.DECIMAL, "Price")
                    .nullable(false)
                    .maximumFractionDigits(2))
            .keyGenerator(automatic("chinook.track"))
            .orderBy(orderBy().ascending(TRACK_NAME))
            .stringProvider(new StringProvider(TRACK_NAME))
            .searchPropertyIds(TRACK_NAME)
            .caption("Tracks");
  }

  void invoice() {
    define(T_INVOICE, "chinook.invoice",
            primaryKeyProperty(INVOICE_INVOICEID, Types.BIGINT, "Invoice no."),
            columnProperty(INVOICE_INVOICEID_AS_STRING, Types.VARCHAR, "Invoice no.")
                    .readOnly(true)
                    .hidden(true),
            foreignKeyProperty(INVOICE_CUSTOMER_FK, "Customer", T_CUSTOMER,
                    columnProperty(INVOICE_CUSTOMERID, Types.BIGINT))
                    .nullable(false),
            columnProperty(INVOICE_INVOICEDATE, Types.TIMESTAMP, "Date/time")
                    .nullable(false),
            columnProperty(INVOICE_BILLINGADDRESS, Types.VARCHAR, "Billing address")
                    .maximumLength(70),
            columnProperty(INVOICE_BILLINGCITY, Types.VARCHAR, "Billing city")
                    .maximumLength(40),
            columnProperty(INVOICE_BILLINGSTATE, Types.VARCHAR, "Billing state")
                    .maximumLength(40),
            columnProperty(INVOICE_BILLINGCOUNTRY, Types.VARCHAR, "Billing country")
                    .maximumLength(40),
            columnProperty(INVOICE_BILLINGPOSTALCODE, Types.VARCHAR, "Billing postal code")
                    .maximumLength(10),
            columnProperty(INVOICE_TOTAL, Types.DECIMAL, "Total")
                    .maximumFractionDigits(2)
                    .hidden(true),
            subqueryProperty(INVOICE_TOTAL_SUB, Types.DECIMAL, "Calculated total",
                    "select sum(unitprice * quantity) from chinook.invoiceline " +
                            "where invoiceid = invoice.invoiceid")
                    .maximumFractionDigits(2))
            .keyGenerator(automatic("chinook.invoice"))
            .orderBy(orderBy().ascending(INVOICE_CUSTOMERID).descending(INVOICE_INVOICEDATE))
            .stringProvider(new StringProvider(INVOICE_INVOICEID))
            .searchPropertyIds(INVOICE_INVOICEID_AS_STRING)
            .caption("Invoices");
  }

  void invoiceLine() {
    define(T_INVOICELINE, "chinook.invoiceline",
            primaryKeyProperty(INVOICELINE_INVOICELINEID, Types.BIGINT),
            foreignKeyProperty(INVOICELINE_INVOICE_FK, "Invoice", T_INVOICE,
                    columnProperty(INVOICELINE_INVOICEID, Types.BIGINT))
                    .fetchDepth(0)
                    .nullable(false),
            foreignKeyProperty(INVOICELINE_TRACK_FK, "Track", T_TRACK,
                    columnProperty(INVOICELINE_TRACKID, Types.BIGINT))
                    .nullable(false)
                    .preferredColumnWidth(100),
            denormalizedProperty(INVOICELINE_UNITPRICE, INVOICELINE_TRACK_FK,
                    getDefinition(T_TRACK).getProperty(TRACK_UNITPRICE), "Unit price")
                    .nullable(false),
            columnProperty(INVOICELINE_QUANTITY, Types.INTEGER, "Quantity")
                    .nullable(false),
            derivedProperty(INVOICELINE_TOTAL, Types.DOUBLE, "Total", INVOICELINE_TOTAL_PROVIDER,
                    INVOICELINE_QUANTITY, INVOICELINE_UNITPRICE))
            .keyGenerator(automatic("chinook.invoiceline"))
            .caption("Invoice lines");
  }

  void playlist() {
    define(T_PLAYLIST, "chinook.playlist",
            primaryKeyProperty(PLAYLIST_PLAYLISTID, Types.BIGINT),
            columnProperty(PLAYLIST_NAME, Types.VARCHAR, "Name")
                    .nullable(false)
                    .maximumLength(120)
                    .preferredColumnWidth(160))
            .keyGenerator(automatic("chinook.playlist"))
            .orderBy(orderBy().ascending(PLAYLIST_NAME))
            .stringProvider(new StringProvider(PLAYLIST_NAME))
            .searchPropertyIds(PLAYLIST_NAME)
            .caption("Playlists");
  }

  void playlistTrack() {
    define(T_PLAYLISTTRACK, "chinook.playlisttrack",
            foreignKeyProperty(PLAYLISTTRACK_PLAYLIST_FK, "Playlist", T_PLAYLIST,
                    primaryKeyProperty(PLAYLISTTRACK_PLAYLISTID, Types.BIGINT)
                            .updatable(true))
                    .nullable(false)
                    .preferredColumnWidth(120),
            denormalizedViewProperty(PLAYLISTTRACK_ARTIST_DENORM, PLAYLISTTRACK_ALBUM_DENORM,
                    getDefinition(T_ALBUM).getProperty(ALBUM_ARTIST_FK), "Artist")
                    .preferredColumnWidth(160),
            foreignKeyProperty(PLAYLISTTRACK_TRACK_FK, "Track", T_TRACK,
                    primaryKeyProperty(PLAYLISTTRACK_TRACKID, Types.BIGINT)
                            .primaryKeyIndex(1)
                            .updatable(true))
                    .fetchDepth(3)
                    .nullable(false)
                    .preferredColumnWidth(160),
            denormalizedViewProperty(PLAYLISTTRACK_ALBUM_DENORM, PLAYLISTTRACK_TRACK_FK,
                    getDefinition(T_TRACK).getProperty(TRACK_ALBUM_FK), "Album")
                    .preferredColumnWidth(160))
            .stringProvider(new StringProvider(PLAYLISTTRACK_PLAYLIST_FK)
                    .addText(" - ").addValue(PLAYLISTTRACK_TRACK_FK))
            .caption("Playlist tracks");
  }

  void dbOperations() {
    addOperation(new UpdateTotalsProcedure());
    addOperation(new RaisePriceFunction());
  }

  private static final class UpdateTotalsProcedure extends AbstractDatabaseProcedure<LocalEntityConnection> {

    private UpdateTotalsProcedure() {
      super(P_UPDATE_TOTALS, "Update invoice totals");
    }

    @Override
    public void execute(final LocalEntityConnection entityConnection,
                        final Object... arguments) throws DatabaseException {
      try {
        entityConnection.beginTransaction();
        final EntitySelectCondition selectCondition = entitySelectCondition(T_INVOICE);
        selectCondition.setForUpdate(true);
        selectCondition.setForeignKeyFetchDepthLimit(0);
        final List<Entity> invoices = entityConnection.select(selectCondition);
        for (final Entity invoice : invoices) {
          invoice.put(INVOICE_TOTAL, invoice.get(INVOICE_TOTAL_SUB));
        }
        final List<Entity> modifiedInvoices = getModifiedEntities(invoices);
        if (!modifiedInvoices.isEmpty()) {
          entityConnection.update(modifiedInvoices);
        }
        entityConnection.commitTransaction();
      }
      catch (final DatabaseException exception) {
        entityConnection.rollbackTransaction();
        throw exception;
      }
    }
  }

  private static final class RaisePriceFunction extends AbstractDatabaseFunction<LocalEntityConnection, List<Entity>> {

    private RaisePriceFunction() {
      super(F_RAISE_PRICE, "Raise track prices");
    }

    @Override
    public List<Entity> execute(final LocalEntityConnection entityConnection,
                        final Object... arguments) throws DatabaseException {
      final List<Long> trackIds = (List<Long>) arguments[0];
      final BigDecimal priceIncrease = (BigDecimal) arguments[1];
      try {
        entityConnection.beginTransaction();

        final EntitySelectCondition selectCondition = entitySelectCondition(T_TRACK,
                TRACK_TRACKID, ConditionType.LIKE, trackIds);
        selectCondition.setForUpdate(true);

        final List<Entity> tracks = entityConnection.select(selectCondition);
        tracks.forEach(track ->
                track.put(TRACK_UNITPRICE,
                        track.getBigDecimal(TRACK_UNITPRICE).add(priceIncrease)));
        final List<Entity> updatedTracks = entityConnection.update(tracks);

        entityConnection.commitTransaction();

        return updatedTracks;
      }
      catch (final DatabaseException exception) {
        entityConnection.rollbackTransaction();
        throw exception;
      }
    }
  }

  private static final class CoverArtImageProvider implements DerivedProperty.Provider {

    @Override
    public Object getValue(final Map<String, Object> sourceValues) {
      final byte[] bytes = (byte[]) sourceValues.get(ALBUM_COVER);
      if (bytes == null) {
        return null;
      }

      try {
        return ImageIO.read(new ByteArrayInputStream(bytes));
      }
      catch (final IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

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

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

      return builder.toString();
    }
  }
}

1.1. Domain unit test

package org.jminor.framework.demos.chinook.domain;

import org.jminor.framework.demos.chinook.domain.impl.ChinookImpl;
import org.jminor.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public class ChinookTest extends EntityTestUnit {

  public ChinookTest() {
    super(ChinookImpl.class.getName());
  }

  @Test
  public void album() throws Exception {
    test(T_ALBUM);
  }

  @Test
  public void artist() throws Exception {
    test(T_ARTIST);
  }

  @Test
  public void customer() throws Exception {
    test(T_CUSTOMER);
  }

  @Test
  public void employee() throws Exception {
    test(T_EMPLOYEE);
  }

  @Test
  public void genre() throws Exception {
    test(T_GENRE);
  }

  @Test
  public void invoce() throws Exception {
    test(T_INVOICE);
  }

  @Test
  public void invoiceLine() throws Exception {
    test(T_INVOICELINE);
  }

  @Test
  public void mediaType() throws Exception {
    test(T_MEDIATYPE);
  }

  @Test
  public void playlist() throws Exception {
    test(T_PLAYLIST);
  }

  @Test
  public void playlistTrack() throws Exception {
    test(T_PLAYLISTTRACK);
  }

  @Test
  public void track() throws Exception {
    test(T_TRACK);
  }
}

2. Model

package org.jminor.framework.demos.chinook.model;

import org.jminor.common.db.exception.DatabaseException;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.demos.chinook.domain.Chinook;
import org.jminor.framework.domain.entity.Entities;
import org.jminor.framework.domain.entity.Entity;
import org.jminor.swing.framework.model.SwingEntityTableModel;

import java.math.BigDecimal;
import java.util.List;

import static org.jminor.framework.demos.chinook.domain.Chinook.F_RAISE_PRICE;
import static org.jminor.framework.demos.chinook.domain.Chinook.TRACK_TRACKID;

public class TrackTableModel extends SwingEntityTableModel {

  public TrackTableModel(final EntityConnectionProvider connectionProvider) {
    super(Chinook.T_TRACK, connectionProvider);
    setEditable(true);
  }

  public void raisePriceOfSelected(final BigDecimal increase) throws DatabaseException {
    final List<Long> trackIds = Entities.getValues(TRACK_TRACKID,
            getSelectionModel().getSelectedItems());

    final List<Entity> result = getConnectionProvider().getConnection()
            .executeFunction(F_RAISE_PRICE, trackIds, increase);
    replaceEntities(result);
  }
}
package org.jminor.framework.demos.chinook.model;

import org.jminor.common.db.exception.DatabaseException;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.swing.framework.model.SwingEntityApplicationModel;
import org.jminor.swing.framework.model.SwingEntityModel;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public final class ChinookApplicationModel extends SwingEntityApplicationModel {

  public ChinookApplicationModel(final EntityConnectionProvider connectionProvider) {
    super(connectionProvider);
    final SwingEntityModel artistModel = new SwingEntityModel(T_ARTIST, connectionProvider);
    final SwingEntityModel albumModel = new SwingEntityModel(T_ALBUM, connectionProvider);
    final SwingEntityModel trackModel = new SwingEntityModel(new TrackTableModel(connectionProvider));

    albumModel.addDetailModel(trackModel);
    artistModel.addDetailModel(albumModel);
    addEntityModel(artistModel);

    final SwingEntityModel playlistModel = new SwingEntityModel(T_PLAYLIST, connectionProvider);
    final SwingEntityModel playlistTrackModel = new SwingEntityModel(T_PLAYLISTTRACK, connectionProvider);

    playlistModel.addDetailModel(playlistTrackModel);
    addEntityModel(playlistModel);

    final SwingEntityModel customerModel = new SwingEntityModel(T_CUSTOMER, connectionProvider);
    final SwingEntityModel invoiceModel = new SwingEntityModel(T_INVOICE, connectionProvider);
    final SwingEntityModel invoiceLineModel = new SwingEntityModel(T_INVOICELINE, connectionProvider);
    invoiceModel.addDetailModel(invoiceLineModel);
    invoiceModel.addLinkedDetailModel(invoiceLineModel);
    customerModel.addDetailModel(invoiceModel);
    addEntityModel(customerModel);

    artistModel.refresh();
    playlistModel.refresh();
    customerModel.refresh();
  }

  public void updateInvoiceTotals() throws DatabaseException {
    getConnectionProvider().getConnection().executeProcedure(P_UPDATE_TOTALS);
  }
}

2.1. Model unit test

package org.jminor.framework.demos.chinook.model;

import org.jminor.common.db.Databases;
import org.jminor.common.db.exception.DatabaseException;
import org.jminor.common.model.table.ColumnConditionModel;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.db.local.LocalEntityConnectionProvider;
import org.jminor.framework.demos.chinook.domain.Chinook;
import org.jminor.framework.demos.chinook.domain.impl.ChinookImpl;
import org.jminor.framework.domain.entity.Entity;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;

public final class TrackTableModelTest {

  @Test
  public void raisePriceOfSelected() throws DatabaseException {
    final EntityConnectionProvider connectionProvider = createConnectionProvider();

    final Entity masterOfPuppets = connectionProvider.getConnection()
            .selectSingle(Chinook.T_ALBUM, Chinook.ALBUM_TITLE, "Master Of Puppets");

    final TrackTableModel trackTableModel = new TrackTableModel(connectionProvider);
    final ColumnConditionModel albumConditionModel =
            trackTableModel.getConditionModel().getPropertyConditionModel(Chinook.TRACK_ALBUM_FK);

    albumConditionModel.setLikeValue(masterOfPuppets);

    trackTableModel.refresh();
    assertEquals(8, trackTableModel.getRowCount());

    trackTableModel.getSelectionModel().selectAll();
    trackTableModel.raisePriceOfSelected(BigDecimal.ONE);

    trackTableModel.getItems().forEach(track ->
            assertEquals(BigDecimal.valueOf(1.99), track.getBigDecimal(Chinook.TRACK_UNITPRICE)));
  }

  private EntityConnectionProvider createConnectionProvider() {
    return new LocalEntityConnectionProvider(Databases.getInstance())
            .setDomainClassName(ChinookImpl.class.getName())
            .setUser(Users.parseUser("scott:tiger"));
  }
}

3. UI

package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public class AlbumEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(ALBUM_ARTIST_FK);

    createForeignKeyLookupField(ALBUM_ARTIST_FK).setColumns(18);
    createTextField(ALBUM_TITLE).setColumns(18);

    final JPanel inputPanel = new JPanel(new GridLayout(2, 1, 5, 5));
    inputPanel.add(createPropertyPanel(ALBUM_ARTIST_FK));
    inputPanel.add(createPropertyPanel(ALBUM_TITLE));

    setLayout(new BorderLayout(5, 5));

    final JPanel inputBasePanel = new JPanel(new BorderLayout(5, 5));
    inputBasePanel.add(inputPanel, BorderLayout.NORTH);

    add(inputBasePanel, BorderLayout.WEST);
    add(new CoverArtPanel(getEditModel().value(ALBUM_COVER)), BorderLayout.CENTER);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.common.value.Value;
import org.jminor.plugin.imagepanel.NavigableImagePanel;
import org.jminor.swing.common.ui.control.Controls;
import org.jminor.swing.common.ui.dialog.Dialogs;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

/**
 * A panel for displaying a cover image, based on a byte array.
 */
final class CoverArtPanel extends JPanel {

  private final NavigableImagePanel imagePanel;
  private final Value<byte[]> imageBytesValue;

  /**
   * @param imageBytesValue the image bytes value to base this panel on.
   */
  CoverArtPanel(final Value<byte[]> imageBytesValue) {
    super(new BorderLayout());
    this.imageBytesValue = imageBytesValue;
    this.imagePanel = createImagePanel();
    initializePanel();
    bindEvents();
  }

  private void initializePanel() {
    final JPanel coverPanel = new JPanel(new BorderLayout());
    coverPanel.setBorder(BorderFactory.createTitledBorder("Cover"));
    coverPanel.add(imagePanel, BorderLayout.CENTER);

    final JPanel coverButtonPanel = new JPanel(new GridLayout(1, 2, 5, 5));
    coverButtonPanel.add(new JButton(Controls.control(this::setCover, "Select cover...")));
    coverButtonPanel.add(new JButton(Controls.control(this::removeCover, "Remove cover")));

    add(coverPanel, BorderLayout.CENTER);
    add(coverButtonPanel, BorderLayout.SOUTH);
  }

  private void bindEvents() {
    imageBytesValue.addDataListener(imageBytes -> imagePanel.setImage(readImage(imageBytes)));
  }

  private void setCover() throws IOException {
    final File coverFile = Dialogs.selectFile(this, null, "Select image");
    imageBytesValue.set(Files.readAllBytes(coverFile.toPath()));
  }

  private void removeCover() {
    imageBytesValue.set(null);
  }

  private static NavigableImagePanel createImagePanel() {
    final NavigableImagePanel panel = new NavigableImagePanel();
    panel.setZoomDevice(NavigableImagePanel.ZoomDevice.NONE);
    panel.setNavigationImageEnabled(false);
    panel.setMoveImageEnabled(false);
    panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    panel.setPreferredSize(new Dimension(200, 200));

    return panel;
  }

  private static BufferedImage readImage(final byte[] bytes) {
    if (bytes == null) {
      return null;
    }
    try {
      return ImageIO.read(new ByteArrayInputStream(bytes));
    }
    catch (final IOException e) {
      throw new RuntimeException(e);
    }
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.ARTIST_NAME;

public class ArtistEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(ARTIST_NAME);

    createTextField(ARTIST_NAME).setColumns(18);

    setLayout(new GridLayout(1, 1, 5, 5));
    addPropertyPanel(ARTIST_NAME);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.common.ui.layout.FlexibleGridLayout;
import org.jminor.swing.common.ui.textfield.TextFields;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public class CustomerEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(CUSTOMER_FIRSTNAME);

    createTextField(CUSTOMER_FIRSTNAME).setColumns(16);
    createTextField(CUSTOMER_LASTNAME).setColumns(16);
    createTextField(CUSTOMER_COMPANY).setColumns(16);
    createTextField(CUSTOMER_ADDRESS).setColumns(16);
    createTextField(CUSTOMER_CITY).setColumns(16);
    TextFields.makeUpperCase(createTextField(CUSTOMER_STATE)).setColumns(16);
    createTextField(CUSTOMER_COUNTRY).setColumns(16);
    createTextField(CUSTOMER_POSTALCODE).setColumns(16);
    createTextField(CUSTOMER_PHONE).setColumns(16);
    createTextField(CUSTOMER_FAX).setColumns(16);
    createTextField(CUSTOMER_EMAIL).setColumns(16);
    createForeignKeyComboBox(CUSTOMER_SUPPORTREP_FK);

    setLayout(new FlexibleGridLayout(4, 3, 5, 5));
    addPropertyPanel(CUSTOMER_FIRSTNAME);
    addPropertyPanel(CUSTOMER_LASTNAME);
    addPropertyPanel(CUSTOMER_COMPANY);
    addPropertyPanel(CUSTOMER_ADDRESS);
    addPropertyPanel(CUSTOMER_CITY);
    addPropertyPanel(CUSTOMER_STATE);
    addPropertyPanel(CUSTOMER_COUNTRY);
    addPropertyPanel(CUSTOMER_POSTALCODE);
    addPropertyPanel(CUSTOMER_PHONE);
    addPropertyPanel(CUSTOMER_FAX);
    addPropertyPanel(CUSTOMER_EMAIL);
    addPropertyPanel(CUSTOMER_SUPPORTREP_FK);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.common.ui.layout.FlexibleGridLayout;
import org.jminor.swing.common.ui.textfield.TextFields;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public class EmployeeEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(EMPLOYEE_FIRSTNAME);

    createTextField(EMPLOYEE_FIRSTNAME).setColumns(16);
    createTextField(EMPLOYEE_LASTNAME).setColumns(16);
    createTemporalInputPanel(EMPLOYEE_BIRTHDATE).getInputField().setColumns(16);
    createTextField(EMPLOYEE_ADDRESS).setColumns(16);
    createTextField(EMPLOYEE_CITY).setColumns(16);
    TextFields.makeUpperCase(createTextField(EMPLOYEE_STATE)).setColumns(16);
    createTextField(EMPLOYEE_COUNTRY).setColumns(16);
    createTextField(EMPLOYEE_POSTALCODE).setColumns(16);
    createTextField(EMPLOYEE_PHONE).setColumns(16);
    createTextField(EMPLOYEE_FAX).setColumns(16);
    createTextField(EMPLOYEE_EMAIL).setColumns(16);
    createForeignKeyComboBox(EMPLOYEE_REPORTSTO_FK);
    createTemporalInputPanel(EMPLOYEE_HIREDATE).getInputField().setColumns(16);
    createTextField(EMPLOYEE_TITLE).setColumns(16);

    setLayout(new FlexibleGridLayout(4, 4, 5, 5));
    addPropertyPanel(EMPLOYEE_FIRSTNAME);
    addPropertyPanel(EMPLOYEE_LASTNAME);
    addPropertyPanel(EMPLOYEE_BIRTHDATE);
    addPropertyPanel(EMPLOYEE_ADDRESS);
    addPropertyPanel(EMPLOYEE_CITY);
    addPropertyPanel(EMPLOYEE_STATE);
    addPropertyPanel(EMPLOYEE_COUNTRY);
    addPropertyPanel(EMPLOYEE_POSTALCODE);
    addPropertyPanel(EMPLOYEE_PHONE);
    addPropertyPanel(EMPLOYEE_FAX);
    addPropertyPanel(EMPLOYEE_EMAIL);
    addPropertyPanel(EMPLOYEE_REPORTSTO_FK);
    addPropertyPanel(EMPLOYEE_HIREDATE);
    addPropertyPanel(EMPLOYEE_TITLE);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.GENRE_NAME;

public class GenreEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(GENRE_NAME);

    createTextField(GENRE_NAME).setColumns(12);

    setLayout(new GridLayout(1, 1, 5, 5));
    addPropertyPanel(GENRE_NAME);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.common.ui.KeyEvents;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import javax.swing.JLabel;
import javax.swing.JTextField;
import java.awt.BorderLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.INVOICELINE_QUANTITY;
import static org.jminor.framework.demos.chinook.domain.Chinook.INVOICELINE_TRACK_FK;

public class InvoiceLineEditPanel extends EntityEditPanel {

  private JTextField tableSearchField;

  public InvoiceLineEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
    editModel.setPersistValue(INVOICELINE_TRACK_FK, false);
  }

  public void setTableSearchFeld(final JTextField tableSearchField) {
    this.tableSearchField = tableSearchField;
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(INVOICELINE_TRACK_FK);

    createForeignKeyLookupField(INVOICELINE_TRACK_FK).setColumns(15);
    final JTextField quantityField = createTextField(INVOICELINE_QUANTITY);
    KeyEvents.removeTransferFocusOnEnter(quantityField);//otherwise the action added below wont work
    quantityField.addActionListener(getSaveControl());

    setLayout(new BorderLayout(5, 5));
    add(createPropertyPanel(INVOICELINE_TRACK_FK), BorderLayout.WEST);
    add(createPropertyPanel(INVOICELINE_QUANTITY), BorderLayout.CENTER);
    add(createPropertyPanel(new JLabel(" "), tableSearchField), BorderLayout.EAST);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.common.model.table.SortingDirective;
import org.jminor.swing.common.ui.time.TemporalInputPanel;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.model.SwingEntityTableModel;
import org.jminor.swing.framework.ui.EntityEditPanel;
import org.jminor.swing.framework.ui.EntityLookupField;
import org.jminor.swing.framework.ui.EntityPanel;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JTextField;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public class InvoiceEditPanel extends EntityEditPanel {

  private EntityPanel invoiceLinePanel;

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

  public void setInvoiceLinePanel(final EntityPanel invoiceLinePanel) {
    this.invoiceLinePanel = invoiceLinePanel;
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(INVOICE_CUSTOMER_FK);
    final EntityLookupField customerField = createForeignKeyLookupField(INVOICE_CUSTOMER_FK);
    configureCustomerLookup(customerField);
    customerField.setColumns(16);
    final TemporalInputPanel datePanel = createTemporalInputPanel(INVOICE_INVOICEDATE);
    datePanel.getInputField().setColumns(16);
    final JTextField addressField = createTextField(INVOICE_BILLINGADDRESS);
    addressField.setColumns(16);
    final JTextField cityField = createTextField(INVOICE_BILLINGCITY);
    cityField.setColumns(16);
    final JTextField stateField = createTextField(INVOICE_BILLINGSTATE);
    stateField.setColumns(16);
    final JTextField countryField = createTextField(INVOICE_BILLINGCOUNTRY);
    countryField.setColumns(16);
    final JTextField postalcodeField = createTextField(INVOICE_BILLINGPOSTALCODE);
    postalcodeField.setColumns(16);
    final JTextField totalField = createTextField(INVOICE_TOTAL_SUB);
    totalField.setColumns(16);

    final JPanel centerPanel = new JPanel(new GridLayout(4, 2, 5, 5));
    centerPanel.add(createPropertyPanel(INVOICE_CUSTOMER_FK));
    centerPanel.add(createPropertyPanel(INVOICE_INVOICEDATE));
    centerPanel.add(createPropertyPanel(INVOICE_BILLINGADDRESS));
    centerPanel.add(createPropertyPanel(INVOICE_BILLINGCITY));
    centerPanel.add(createPropertyPanel(INVOICE_BILLINGSTATE));
    centerPanel.add(createPropertyPanel(INVOICE_BILLINGCOUNTRY));
    centerPanel.add(createPropertyPanel(INVOICE_BILLINGPOSTALCODE));
    centerPanel.add(createPropertyPanel(INVOICE_TOTAL_SUB));

    final JPanel centerBasePanel = new JPanel(new BorderLayout(5, 5));
    centerBasePanel.add(centerPanel, BorderLayout.CENTER);

    invoiceLinePanel.setBorder(BorderFactory.createTitledBorder("Invoice lines"));

    setLayout(new BorderLayout(5, 5));
    add(centerBasePanel, BorderLayout.CENTER);
    add(invoiceLinePanel, BorderLayout.EAST);
  }

  private void configureCustomerLookup(final EntityLookupField customerField) {
    final EntityLookupField.TableSelectionProvider customerSelectionProvider =
            new EntityLookupField.TableSelectionProvider(customerField.getModel());
    final SwingEntityTableModel tableModel = customerSelectionProvider.getTable().getModel();
    tableModel.setColumns(CUSTOMER_LASTNAME, CUSTOMER_FIRSTNAME, CUSTOMER_EMAIL);
    tableModel.setSortingDirective(CUSTOMER_LASTNAME, SortingDirective.ASCENDING);
    customerSelectionProvider.setPreferredSize(new Dimension(500, 300));
    customerField.setSelectionProvider(customerSelectionProvider);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.MEDIATYPE_NAME;

public class MediaTypeEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(MEDIATYPE_NAME);

    createTextField(MEDIATYPE_NAME).setColumns(12);

    setLayout(new GridLayout(1, 1, 5, 5));
    addPropertyPanel(MEDIATYPE_NAME);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.PLAYLIST_NAME;

public class PlaylistEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(PLAYLIST_NAME);

    createTextField(PLAYLIST_NAME).setColumns(12);

    setLayout(new GridLayout(1, 1, 5, 5));
    addPropertyPanel(PLAYLIST_NAME);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.PLAYLISTTRACK_PLAYLIST_FK;
import static org.jminor.framework.demos.chinook.domain.Chinook.PLAYLISTTRACK_TRACK_FK;

public class PlaylistTrackEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(PLAYLISTTRACK_PLAYLIST_FK);

    createForeignKeyComboBox(PLAYLISTTRACK_PLAYLIST_FK);
    createForeignKeyLookupField(PLAYLISTTRACK_TRACK_FK).setColumns(30);

    setLayout(new GridLayout(2, 1, 5, 5));
    addPropertyPanel(PLAYLISTTRACK_PLAYLIST_FK);
    addPropertyPanel(PLAYLISTTRACK_TRACK_FK);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.swing.common.ui.Components;
import org.jminor.swing.common.ui.layout.FlexibleGridLayout;
import org.jminor.swing.common.ui.textfield.IntegerField;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityComboBox;
import org.jminor.swing.framework.ui.EntityEditPanel;
import org.jminor.swing.framework.ui.EntityPanelBuilder;

import javax.swing.Action;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import java.awt.GridLayout;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public class TrackEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(TRACK_ALBUM_FK);

    createForeignKeyLookupField(TRACK_ALBUM_FK).setColumns(18);
    createTextField(TRACK_NAME).setColumns(18);
    final EntityComboBox mediaTypeBox = createForeignKeyComboBox(TRACK_MEDIATYPE_FK);
    final Action newMediaTypeAction = EntityEditPanel.createEditPanelAction(mediaTypeBox,
            new EntityPanelBuilder(T_MEDIATYPE)
                    .setEditPanelClass(MediaTypeEditPanel.class));
    final JPanel mediaTypePanel = Components.createEastButtonPanel(mediaTypeBox, newMediaTypeAction, false);
    final EntityComboBox genreBox = createForeignKeyComboBox(TRACK_GENRE_FK);
    final Action newGenreAction = EntityEditPanel.createEditPanelAction(genreBox,
            new EntityPanelBuilder(T_GENRE)
                    .setEditPanelClass(GenreEditPanel.class));
    final JPanel genrePanel = Components.createEastButtonPanel(genreBox, newGenreAction, false);
    createTextInputPanel(TRACK_COMPOSER).getTextField().setColumns(18);
    final IntegerField millisecondsField = (IntegerField) createTextField(TRACK_MILLISECONDS);
    millisecondsField.setGroupingUsed(true);
    final IntegerField bytesField = (IntegerField) createTextField(TRACK_BYTES);
    bytesField.setGroupingUsed(true);
    bytesField.setColumns(18);
    createTextField(TRACK_UNITPRICE).setColumns(18);
    final JTextField durationField = createTextField(TRACK_MINUTES_SECONDS_DERIVED);
    final JPanel durationPanel = new JPanel(new GridLayout(1, 2, 5, 5));
    durationPanel.add(createPropertyPanel(TRACK_MILLISECONDS, millisecondsField));
    durationPanel.add(createPropertyPanel(new JLabel("(min/sec)"), durationField));

    setLayout(new FlexibleGridLayout(4, 2, 5, 5, true, false));
    addPropertyPanel(TRACK_ALBUM_FK);
    addPropertyPanel(TRACK_NAME);
    add(createPropertyPanel(TRACK_GENRE_FK, genrePanel));
    addPropertyPanel(TRACK_COMPOSER);
    add(createPropertyPanel(TRACK_MEDIATYPE_FK, mediaTypePanel));
    addPropertyPanel(TRACK_BYTES);
    addPropertyPanel(TRACK_UNITPRICE);
    add(durationPanel);
  }
}
package org.jminor.framework.demos.chinook.ui;

import org.jminor.common.db.exception.DatabaseException;
import org.jminor.common.model.CancelException;
import org.jminor.framework.demos.chinook.model.TrackTableModel;
import org.jminor.swing.common.ui.control.ControlSet;
import org.jminor.swing.common.ui.control.Controls;
import org.jminor.swing.common.ui.dialog.Dialogs;
import org.jminor.swing.common.ui.dialog.Modal;
import org.jminor.swing.common.ui.textfield.DecimalField;
import org.jminor.swing.common.ui.value.ComponentValuePanel;
import org.jminor.swing.common.ui.value.NumericalValues;
import org.jminor.swing.framework.model.SwingEntityTableModel;
import org.jminor.swing.framework.ui.EntityTablePanel;

import java.math.BigDecimal;
import java.util.List;

public class TrackTablePanel extends EntityTablePanel {

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

  @Override
  protected ControlSet getPopupControls(final List<ControlSet> additionalPopupControlSets) {
    final ControlSet controls = super.getPopupControls(additionalPopupControlSets);
    controls.addAt(0, Controls.control(this::raisePriceOfSelected, "Raise price...",
            getTableModel().getSelectionModel().getSelectionNotEmptyObserver()));
    controls.addSeparatorAt(1);

    return controls;
  }

  private void raisePriceOfSelected() throws DatabaseException {
    final TrackTableModel tableModel = (TrackTableModel) getTableModel();

    tableModel.raisePriceOfSelected(getAmountFromUser());
  }

  private BigDecimal getAmountFromUser() {
    final ComponentValuePanel<BigDecimal, DecimalField> inputPanel =
            new ComponentValuePanel<>("Amount",
                    NumericalValues.bigDecimalValue(new DecimalField()));
    Dialogs.displayInDialog(this, inputPanel, "Price Raise", Modal.YES,
            inputPanel.getOkAction(), inputPanel.getButtonClickObserver());
    if (inputPanel.isInputAccepted() && inputPanel.getValue() != null) {
      return inputPanel.getValue();
    }

    throw new CancelException();
  }
}

3.1. Main application panel

package org.jminor.framework.demos.chinook.ui;

import org.jminor.common.model.CancelException;
import org.jminor.common.model.table.ColumnConditionModel;
import org.jminor.common.user.Users;
import org.jminor.common.version.Version;
import org.jminor.common.version.Versions;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.demos.chinook.model.ChinookApplicationModel;
import org.jminor.framework.model.EntityEditModel;
import org.jminor.swing.common.ui.Windows;
import org.jminor.swing.common.ui.control.ControlSet;
import org.jminor.swing.common.ui.control.Controls;
import org.jminor.swing.framework.model.SwingEntityModel;
import org.jminor.swing.framework.ui.EntityApplicationPanel;
import org.jminor.swing.framework.ui.EntityPanel;
import org.jminor.swing.framework.ui.EntityPanelBuilder;
import org.jminor.swing.framework.ui.EntityTablePanel;

import javax.swing.JTable;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static org.jminor.framework.demos.chinook.domain.Chinook.*;
import static org.jminor.swing.common.ui.worker.ProgressWorker.runWithProgressBar;

public final class ChinookAppPanel extends EntityApplicationPanel<ChinookApplicationModel> {

  /* ARTIST
   *   ALBUM
   *     TRACK
   * PLAYLIST
   *   PLAYLISTTRACK
   * CUSTOMER
   *   INVOICE
   *     INVOICELINE
   */
  @Override
  protected void setupEntityPanelBuilders() {
    final EntityPanelBuilder trackBuilder = new EntityPanelBuilder(T_TRACK);
    trackBuilder.setEditPanelClass(TrackEditPanel.class).setTablePanelClass(TrackTablePanel.class);

    final EntityPanelBuilder customerBuilder = new EntityPanelBuilder(T_CUSTOMER);
    customerBuilder.setEditPanelClass(CustomerEditPanel.class);
    customerBuilder.setTablePanelClass(CustomerTablePanel.class);

    final EntityPanelBuilder genreBuilder = new EntityPanelBuilder(T_GENRE);
    genreBuilder.setEditPanelClass(GenreEditPanel.class);
    genreBuilder.addDetailPanelBuilder(trackBuilder).setDetailPanelState(EntityPanel.PanelState.HIDDEN);

    final EntityPanelBuilder mediaTypeBuilder = new EntityPanelBuilder(T_MEDIATYPE);
    mediaTypeBuilder.setEditPanelClass(MediaTypeEditPanel.class);
    mediaTypeBuilder.addDetailPanelBuilder(trackBuilder).setDetailPanelState(EntityPanel.PanelState.HIDDEN);

    final EntityPanelBuilder employeeBuilder = new EntityPanelBuilder(T_EMPLOYEE);
    employeeBuilder.setEditPanelClass(EmployeeEditPanel.class);
    employeeBuilder.addDetailPanelBuilder(customerBuilder).setDetailPanelState(EntityPanel.PanelState.HIDDEN);

    addSupportPanelBuilders(genreBuilder, mediaTypeBuilder, employeeBuilder);
  }

  @Override
  protected List<EntityPanel> initializeEntityPanels(final ChinookApplicationModel applicationModel) {
    final List<EntityPanel> panels = new ArrayList<>();

    final SwingEntityModel artistModel = applicationModel.getEntityModel(T_ARTIST);
    final EntityPanel artistPanel = new EntityPanel(artistModel, new ArtistEditPanel(artistModel.getEditModel()));
    final SwingEntityModel albumModel = artistModel.getDetailModel(T_ALBUM);
    final EntityPanel albumPanel = new EntityPanel(albumModel, new AlbumEditPanel(albumModel.getEditModel()));
    final SwingEntityModel trackModel = albumModel.getDetailModel(T_TRACK);
    final EntityPanel trackPanel = new EntityPanel(trackModel,
            new TrackEditPanel(trackModel.getEditModel()), new TrackTablePanel(trackModel.getTableModel()));

    albumPanel.addDetailPanel(trackPanel);
    artistPanel.addDetailPanel(albumPanel);
    panels.add(artistPanel);

    final SwingEntityModel playlistModel = applicationModel.getEntityModel(T_PLAYLIST);
    final EntityPanel playlistPanel = new EntityPanel(playlistModel, new PlaylistEditPanel(playlistModel.getEditModel()));
    final SwingEntityModel playlistTrackModel = playlistModel.getDetailModel(T_PLAYLISTTRACK);
    final EntityPanel playlistTrackPanel = new EntityPanel(playlistTrackModel, new PlaylistTrackEditPanel(playlistTrackModel.getEditModel()));

    playlistPanel.addDetailPanel(playlistTrackPanel);
    panels.add(playlistPanel);

    final SwingEntityModel customerModel = applicationModel.getEntityModel(T_CUSTOMER);
    final EntityPanel customerPanel = new EntityPanel(customerModel, new CustomerEditPanel(customerModel.getEditModel()),
            new CustomerTablePanel(customerModel.getTableModel()));
    final SwingEntityModel invoiceModel = customerModel.getDetailModel(T_INVOICE);
    final EntityPanel invoicePanel = new EntityPanel(invoiceModel, new InvoiceEditPanel(invoiceModel.getEditModel()));
    invoicePanel.setIncludeDetailPanelTabPane(false);

    final SwingEntityModel invoiceLineModel = invoiceModel.getDetailModel(T_INVOICELINE);
    final EntityPanel invoiceLinePanel = new EntityPanel(invoiceLineModel, new InvoiceLineEditPanel(invoiceLineModel.getEditModel()));
    final EntityTablePanel invoiceLineTablePanel = invoiceLinePanel.getTablePanel();
    invoiceLineTablePanel.setIncludeSouthPanel(false);
    invoiceLineTablePanel.setIncludeConditionPanel(false);
    invoiceLineTablePanel.getTable().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    invoiceLineTablePanel.setPreferredSize(new Dimension(360, 40));
    invoiceLineTablePanel.getTable().getModel().getColumnModel().hideColumn(
            getModel().getDomain().getDefinition(T_INVOICELINE).getProperty(INVOICELINE_INVOICE_FK));
    invoiceLinePanel.setIncludeControlPanel(false);
    ((InvoiceLineEditPanel) invoiceLinePanel.getEditPanel()).setTableSearchFeld(invoiceLinePanel.getTablePanel().getTable().getSearchField());
    invoiceLinePanel.initializePanel();
    ((InvoiceEditPanel) invoicePanel.getEditPanel()).setInvoiceLinePanel(invoiceLinePanel);

    invoicePanel.addDetailPanel(invoiceLinePanel);
    customerPanel.addDetailPanel(invoicePanel);
    panels.add(customerPanel);

    return panels;
  }

  @Override
  protected ChinookApplicationModel initializeApplicationModel(final EntityConnectionProvider connectionProvider) throws CancelException {
    return new ChinookApplicationModel(connectionProvider);
  }

  @Override
  protected Version getClientVersion() {
    return Versions.version(0, 1, 0);
  }

  @Override
  protected ControlSet getToolsControlSet() {
    final ControlSet tools = super.getToolsControlSet();
    tools.addSeparator();
    tools.add(Controls.control(this::updateInvoiceTotals, "Update invoice totals"));

    return tools;
  }

  private void updateInvoiceTotals() {
    runWithProgressBar(this, "Updating totals...",
            "Totals updated", "Updating totals failed",
            getModel()::updateInvoiceTotals);
  }

  public static void main(final String[] args) throws CancelException {
    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.chinook.domain.impl.ChinookImpl");
    new ChinookAppPanel().startApplication("Chinook", null, false,
            Windows.getScreenSizeRatio(0.6), Users.parseUser("scott:tiger"));
  }
}

4. Login proxy

package org.jminor.framework.demos.chinook.server;

import org.jminor.common.db.Database;
import org.jminor.common.db.Databases;
import org.jminor.common.db.exception.DatabaseException;
import org.jminor.common.db.pool.ConnectionPool;
import org.jminor.common.db.pool.ConnectionPoolProvider;
import org.jminor.common.remote.LoginProxy;
import org.jminor.common.remote.RemoteClient;
import org.jminor.common.remote.exception.LoginException;
import org.jminor.common.remote.exception.ServerAuthenticationException;
import org.jminor.common.user.User;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnection;
import org.jminor.framework.db.local.LocalEntityConnections;
import org.jminor.framework.demos.chinook.domain.impl.ChinookImpl;
import org.jminor.framework.domain.Domain;

import static java.lang.String.valueOf;
import static org.jminor.common.Conjunction.AND;
import static org.jminor.common.db.ConditionType.LIKE;
import static org.jminor.common.remote.Servers.remoteClient;
import static org.jminor.framework.db.condition.Conditions.*;
import static org.jminor.framework.demos.chinook.domain.Chinook.*;

/**
 * A {@link org.jminor.common.LoggerProxy} implementation
 * authenticating via a user lookup table.
 */
public final class ChinookLoginProxy implements LoginProxy {

  /**
   * The actual user credentials to return for successfully
   * authenticated users.
   */
  private final User databaseUser = Users.parseUser("scott:tiger");

  /**
   * The Database instance we're connecting to.
   */
  private final Database database = Databases.getInstance();

  /**
   * The Domain on which to base the authentication connection.
   */
  private final Domain domain = new ChinookImpl();

  /**
   * The ConnectionPool used when authenticating users.
   */
  private final ConnectionPool connectionPool;

  public ChinookLoginProxy() throws DatabaseException {
    connectionPool = ConnectionPoolProvider.getConnectionPoolProvider()
            .createConnectionPool(databaseUser, database);
  }

  /**
   * Handles logins from clients with this id
   */
  @Override
  public String getClientTypeId() {
    return "org.jminor.framework.demos.chinook.ui.ChinookAppPanel";
  }

  @Override
  public RemoteClient doLogin(final RemoteClient remoteClient)
          throws LoginException {
    authenticateUser(remoteClient.getUser());

    //Create a new RemoteClient based on the one received
    //but with the actual database user
    return remoteClient(remoteClient, databaseUser);
  }

  @Override
  public void doLogout(final RemoteClient remoteClient) {}

  @Override
  public void close() {
    connectionPool.close();
  }

  private void authenticateUser(final User user)
          throws LoginException {
    final EntityConnection connection = getConnectionFromPool();
    try {
      final int rows = connection.selectRowCount(
              entityCondition(T_USER, conditionSet(AND,
                      propertyCondition(USER_USERNAME,
                              LIKE, user.getUsername()).setCaseSensitive(false),
                      propertyCondition(USER_PASSWORD_HASH,
                              LIKE, valueOf(user.getPassword()).hashCode()))));
      if (rows == 0) {
        throw new ServerAuthenticationException("Wrong username or password");
      }
    }
    catch (final DatabaseException e) {
      throw new RuntimeException(e);
    }
    finally {
      connection.disconnect();//returns the underlying connection to the pool
    }
  }

  private EntityConnection getConnectionFromPool() {
    try {
      return LocalEntityConnections.createConnection(domain, database,
              connectionPool.getConnection());
    }
    catch (final DatabaseException e) {
      throw new RuntimeException(e);
    }
  }
}

5. Load test

package org.jminor.framework.demos.chinook.testing;

import org.jminor.common.db.reports.ReportWrapper;
import org.jminor.common.model.CancelException;
import org.jminor.common.user.User;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnectionProviders;
import org.jminor.framework.demos.chinook.model.ChinookApplicationModel;
import org.jminor.framework.demos.chinook.ui.ChinookAppPanel;
import org.jminor.framework.domain.entity.Entities;
import org.jminor.framework.domain.entity.Entity;
import org.jminor.framework.model.EntityComboBoxModel;
import org.jminor.framework.model.EntityEditModel;
import org.jminor.plugin.jasperreports.model.JasperReportsWrapper;
import org.jminor.swing.common.tools.ui.LoadTestPanel;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.model.SwingEntityModel;
import org.jminor.swing.framework.model.SwingEntityTableModel;
import org.jminor.swing.framework.model.reporting.EntityReportUtil;
import org.jminor.swing.framework.tools.EntityLoadTestModel;

import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Random;

import static java.util.Arrays.asList;
import static org.jminor.framework.demos.chinook.domain.Chinook.*;

public final class ChinookLoadTest extends EntityLoadTestModel<ChinookApplicationModel> {

  private static final User UNIT_TEST_USER =
          Users.parseUser(System.getProperty("jminor.test.user", "scott:tiger"));

  private static final UsageScenario<ChinookApplicationModel> UPDATE_TOTALS =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("updateTotals") {
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              try {
                final SwingEntityModel customerModel = application.getEntityModel(T_CUSTOMER);
                customerModel.getTableModel().refresh();
                selectRandomRows(customerModel.getTableModel(), RANDOM.nextInt(6) + 2);
                final SwingEntityModel invoiceModel = customerModel.getDetailModel(T_INVOICE);
                selectRandomRows(invoiceModel.getTableModel(), RANDOM.nextInt(6) + 2);
                final SwingEntityTableModel invoiceLineTableModel = invoiceModel.getDetailModel(T_INVOICELINE).getTableModel();
                final List<Entity> invoiceLines = invoiceLineTableModel.getItems();
                Entities.put(INVOICELINE_QUANTITY, RANDOM.nextInt(4) + 1, invoiceLines);

                invoiceLineTableModel.update(invoiceLines);

                application.updateInvoiceTotals();
              }
              catch (final Exception e) {
                throw new ScenarioException(e);
              }
            }
          };

  private static final UsageScenario<ChinookApplicationModel> VIEW_GENRE =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("viewGenre") {
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              try {
                final SwingEntityModel genreModel = application.getEntityModel(T_GENRE);
                genreModel.getTableModel().refresh();
                selectRandomRow(genreModel.getTableModel());
                final SwingEntityModel trackModel = genreModel.getDetailModel(T_TRACK);
                selectRandomRows(trackModel.getTableModel(), 2);
                genreModel.getConnectionProvider().getConnection().selectDependencies(trackModel.getTableModel().getSelectionModel().getSelectedItems());
              }
              catch (final Exception e) {
                throw new ScenarioException(e);
              }
            }

            @Override
            public int getDefaultWeight() {
              return 10;
            }
          };

  private static final UsageScenario<ChinookApplicationModel> VIEW_CUSTOMER_REPORT =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("viewCustomerReport") {
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              try {
                final SwingEntityTableModel customerModel = application.getEntityModel(T_CUSTOMER).getTableModel();
                customerModel.refresh();
                selectRandomRow(customerModel);

                final String reportPath = ReportWrapper.getReportPath() + "/customer_report.jasper";
                final Collection<Long> customerIDs =
                        Entities.getDistinctValues(CUSTOMER_CUSTOMERID, customerModel.getSelectionModel().getSelectedItems());
                final HashMap<String, Object> reportParameters = new HashMap<>();
                reportParameters.put("CUSTOMER_IDS", customerIDs);
                EntityReportUtil.fillReport(new JasperReportsWrapper(reportPath, reportParameters),
                        customerModel.getConnectionProvider());
              }
              catch (final Exception e) {
                throw new ScenarioException(e);
              }
            }

            @Override
            public int getDefaultWeight() {
              return 2;
            }
          };

  private static final UsageScenario<ChinookApplicationModel> VIEW_INVOICE =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("viewInvoice") {
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              try {
                final SwingEntityModel customerModel = application.getEntityModel(T_CUSTOMER);
                customerModel.getTableModel().refresh();
                selectRandomRow(customerModel.getTableModel());
                final SwingEntityModel invoiceModel = customerModel.getDetailModel(T_INVOICE);
                selectRandomRow(invoiceModel.getTableModel());
              }
              catch (final Exception e) {
                throw new ScenarioException(e);
              }
            }

            @Override
            public int getDefaultWeight() {
              return 10;
            }
          };

  private static final UsageScenario<ChinookApplicationModel> VIEW_ALBUM =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("viewAlbum") {
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              try {
                final SwingEntityModel artistModel = application.getEntityModel(T_ARTIST);
                artistModel.getTableModel().refresh();
                selectRandomRow(artistModel.getTableModel());
                final SwingEntityModel albumModel = artistModel.getDetailModel(T_ALBUM);
                selectRandomRow(albumModel.getTableModel());
              }
              catch (final Exception e) {
                throw new ScenarioException(e);
              }
            }

            @Override
            public int getDefaultWeight() {
              return 10;
            }
          };

  private static final UsageScenario<ChinookApplicationModel> INSERT_DELETE_ALBUM =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("insertDeleteAlbum") {
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              final SwingEntityModel artistModel = application.getEntityModel(T_ARTIST);
              artistModel.getTableModel().refresh();
              selectRandomRow(artistModel.getTableModel());
              final Entity artist = artistModel.getTableModel().getSelectionModel().getSelectedItem();
              final SwingEntityModel albumModel = artistModel.getDetailModel(T_ALBUM);
              final EntityEditModel albumEditModel = albumModel.getEditModel();
              final Entity album = application.getDomain().entity(T_ALBUM);
              album.put(ALBUM_ARTIST_FK, artist);
              album.put(ALBUM_TITLE, "Title");

              albumEditModel.setEntity(album);
              try {
                final Entity insertedAlbum = albumEditModel.insert();
                final SwingEntityEditModel trackEditModel = albumModel.getDetailModel(T_TRACK).getEditModel();
                final EntityComboBoxModel genreComboBoxModel = trackEditModel.getForeignKeyComboBoxModel(TRACK_GENRE_FK);
                selectRandomItem(genreComboBoxModel);
                final EntityComboBoxModel mediaTypeComboBoxModel = trackEditModel.getForeignKeyComboBoxModel(TRACK_MEDIATYPE_FK);
                selectRandomItem(mediaTypeComboBoxModel);
                for (int i = 0; i < 10; i++) {
                  trackEditModel.put(TRACK_ALBUM_FK, insertedAlbum);
                  trackEditModel.put(TRACK_NAME, "Track " + i);
                  trackEditModel.put(TRACK_BYTES, 10000000);
                  trackEditModel.put(TRACK_COMPOSER, "Composer");
                  trackEditModel.put(TRACK_MILLISECONDS, 1000000);
                  trackEditModel.put(TRACK_UNITPRICE, BigDecimal.valueOf(2));
                  trackEditModel.put(TRACK_GENRE_FK, genreComboBoxModel.getSelectedValue());
                  trackEditModel.put(TRACK_MEDIATYPE_FK, mediaTypeComboBoxModel.getSelectedValue());
                  trackEditModel.insert();
                }

                final SwingEntityTableModel trackTableModel = albumModel.getDetailModel(T_TRACK).getTableModel();
                trackTableModel.getSelectionModel().selectAll();
                trackTableModel.deleteSelected();
                albumEditModel.delete();
              }
              catch (final Exception e) {
                throw new ScenarioException(e);
              }
            }

            @Override
            public int getDefaultWeight() {
              return 3;
            }
          };

  private static final UsageScenario<ChinookApplicationModel> LOGOUT_LOGIN =
          new AbstractEntityUsageScenario<ChinookApplicationModel>("logoutLogin") {
            final Random random = new Random();
            @Override
            protected void performScenario(final ChinookApplicationModel application) throws ScenarioException {
              try {
                application.getConnectionProvider().disconnect();
                Thread.sleep(random.nextInt(1500));
                application.getConnectionProvider().getConnection();
              }
              catch (final InterruptedException ignored) {/*ignored*/}
            }

            @Override
            public int getDefaultWeight() {
              return 0;
            }
          };

  public ChinookLoadTest() {
    super(UNIT_TEST_USER, asList(VIEW_GENRE, VIEW_CUSTOMER_REPORT, VIEW_INVOICE, VIEW_ALBUM,
            UPDATE_TOTALS, INSERT_DELETE_ALBUM, LOGOUT_LOGIN));
  }

  @Override
  protected ChinookApplicationModel initializeApplication() throws CancelException {
    final ChinookApplicationModel applicationModel = new ChinookApplicationModel(
            EntityConnectionProviders.connectionProvider().setDomainClassName("org.jminor.framework.demos.chinook.domain.impl.ChinookImpl")
                    .setClientTypeId(ChinookAppPanel.class.getName()).setUser(getUser()));
    /* ARTIST
     *   ALBUM
     *     TRACK
     * GENRE
     *   GENRETRACK
     * PLAYLIST
     *   PLAYLISTTRACK
     * CUSTOMER
     *   INVOICE
     *     INVOICELINE
     */
    final SwingEntityModel artistModel = applicationModel.getEntityModel(T_ARTIST);
    final SwingEntityModel albumModel = artistModel.getDetailModel(T_ALBUM);
    final SwingEntityModel trackModel = albumModel.getDetailModel(T_TRACK);
    artistModel.addLinkedDetailModel(albumModel);
    albumModel.addLinkedDetailModel(trackModel);

    final SwingEntityModel playlistModel = applicationModel.getEntityModel(T_PLAYLIST);
    final SwingEntityModel playlistTrackModel = playlistModel.getDetailModel(T_PLAYLISTTRACK);
    playlistModel.addLinkedDetailModel(playlistTrackModel);

    final SwingEntityModel customerModel = applicationModel.getEntityModel(T_CUSTOMER);
    final SwingEntityModel invoiceModel = customerModel.getDetailModel(T_INVOICE);
    final SwingEntityModel invoicelineModel = invoiceModel.getDetailModel(T_INVOICELINE);
    customerModel.addLinkedDetailModel(invoiceModel);
    invoiceModel.addLinkedDetailModel(invoicelineModel);

    final SwingEntityModel genreModel = new SwingEntityModel(T_GENRE, applicationModel.getConnectionProvider());
    final SwingEntityModel genreTrackModel = new SwingEntityModel(T_TRACK, applicationModel.getConnectionProvider());
    genreModel.addDetailModel(genreTrackModel);
    genreModel.addLinkedDetailModel(genreTrackModel);

    applicationModel.addEntityModel(genreModel);

    return applicationModel;
  }

  public static void main(final String[] args) throws Exception {
    SwingUtilities.invokeLater(new Runner());
  }

  private static final class Runner implements Runnable {
    @Override
    public void run() {
      try {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

        new LoadTestPanel(new ChinookLoadTest()).showFrame();
      }
      catch (final Exception e) {
        throw new RuntimeException(e);
      }
    }
  }
}