This tutorial assumes you have at least skimmed the Domain model part of the Codion manual.

Domain model

API

public interface World {

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

The domain API sections below continue the World class.

Implementation

public final class WorldImpl extends DefaultDomain implements World {

  private static final List<Item<String>> CONTINENT_ITEMS = List.of(
          item("Africa"), item("Antarctica"), item("Asia"),
          item("Europe"), item("North America"), item("Oceania"),
          item("South America"));

  public WorldImpl() {
    super(World.DOMAIN);
    //By default, you can't define a foreign key referencing an entity which
    //has not been defined, to prevent mistakes. But sometimes we have to
    //deal with cyclical dependencies, such as here, where city references
    //country and country references city. In these cases we can simply
    //disable strict foreign keys.
    setStrictForeignKeys(false);

    city();
    country();
    countryLanguage();
    lookup();
    continent();
  }

The domain implementation sections below continue the WorldImpl class.

Countries

countries

Country

SQL

create table world.country (
  code varchar(3) not null,
  name varchar(52) not null,
  continent varchar(20) not null,
  region varchar(26) not null,
  surfacearea decimal(10,2) not null,
  indepyear smallint,
  population int not null,
  lifeexpectancy decimal(3,1),
  gnp decimal(10,2),
  gnpold decimal(10,2),
  localname varchar(45) not null,
  governmentform varchar(45) not null,
  headofstate varchar(60),
  capital int,
  code2 varchar(2) not null,
  flag blob,
  constraint country_pk primary key (code),
  constraint continent_chk check(continent in ('Asia','Europe','North America','Africa','Oceania','Antarctica','South America'))
);

Domain

API
  interface Country extends Entity {
    EntityType TYPE = DOMAIN.entityType("world.country", Country.class);

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

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

    FunctionType<EntityConnection, String, Double> AVERAGE_CITY_POPULATION = functionType("average_city_population");

    String name();
    String continent();
    String region();
    double surfacearea();
    int population();
  }
Implementation
  void country() {
    add(Country.TYPE.define(
            Country.CODE.define()
                    .primaryKey()
                    .caption("Country code")
                    .updatable(true)
                    .maximumLength(3),
            Country.NAME.define()
                    .column()
                    .caption("Name")
                    .searchable(true)
                    .nullable(false)
                    .maximumLength(52),
            Country.CONTINENT.define()
                    .column()
                    .caption("Continent")
                    .items(CONTINENT_ITEMS)
                    .nullable(false),
            Country.REGION.define()
                    .column()
                    .caption("Region")
                    .nullable(false)
                    .maximumLength(26),
            Country.SURFACEAREA.define()
                    .column()
                    .caption("Surface area")
                    .nullable(false)
                    .numberFormatGrouping(true)
                    .maximumFractionDigits(2),
            Country.INDEPYEAR.define()
                    .column()
                    .caption("Indep. year")
                    .valueRange(-2000, 2500),
            Country.INDEPYEAR_SEARCHABLE.define()
                    .column()
                    .expression("to_char(indepyear)")
                    .searchable(true)
                    .readOnly(true),
            Country.POPULATION.define()
                    .column()
                    .caption("Population")
                    .nullable(false)
                    .numberFormatGrouping(true),
            Country.LIFE_EXPECTANCY.define()
                    .column()
                    .caption("Life expectancy")
                    .maximumFractionDigits(1)
                    .valueRange(0, 99),
            Country.GNP.define()
                    .column()
                    .caption("GNP")
                    .numberFormatGrouping(true)
                    .maximumFractionDigits(2),
            Country.GNPOLD.define()
                    .column()
                    .caption("GNP old")
                    .numberFormatGrouping(true)
                    .maximumFractionDigits(2),
            Country.LOCALNAME.define()
                    .column()
                    .caption("Local name")
                    .nullable(false)
                    .maximumLength(45),
            Country.GOVERNMENTFORM.define()
                    .column()
                    .caption("Government form")
                    .nullable(false),
            Country.HEADOFSTATE.define()
                    .column()
                    .caption("Head of state")
                    .maximumLength(60),
            Country.CAPITAL.define()
                    .column(),
            Country.CAPITAL_FK.define()
                    .foreignKey()
                    .caption("Capital"),
            Country.CAPITAL_POPULATION.define()
                    .denormalized(Country.CAPITAL_FK, City.POPULATION)
                    .caption("Capital pop.")
                    .numberFormatGrouping(true),
            Country.NO_OF_CITIES.define()
                    .subquery("""
                            select count(*)
                            from world.city
                            where city.countrycode = country.code""")
                    .caption("No. of cities"),
            Country.NO_OF_LANGUAGES.define()
                    .subquery("""
                            select count(*)
                            from world.countrylanguage
                            where countrylanguage.countrycode = country.code""")
                    .caption("No. of languages"),
            Country.FLAG.define()
                    .column()
                    .caption("Flag")
                    .lazy(true),
            Country.CODE_2.define()
                    .column()
                    .caption("Code2")
                    .nullable(false)
                    .maximumLength(2))
            .orderBy(ascending(Country.NAME))
            .stringFactory(Country.NAME)
            .caption("Country"));

    add(Country.AVERAGE_CITY_POPULATION, new AverageCityPopulationFunction());
  }
  private static final class AverageCityPopulationFunction implements DatabaseFunction<EntityConnection, String, Double> {

    @Override
    public Double execute(EntityConnection connection, String countryCode) throws DatabaseException {
      return connection.select(City.POPULATION,
                      City.COUNTRY_CODE.equalTo(countryCode))
              .stream()
              .mapToInt(Integer::intValue)
              .average()
              .orElse(0d);
    }
  }

Model

CountryModel
package is.codion.framework.demos.world.model;

import is.codion.framework.db.EntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityModel;

public final class CountryModel extends SwingEntityModel {

  CountryModel(EntityConnectionProvider connectionProvider) {
    super(new CountryTableModel(connectionProvider));
    SwingEntityModel cityModel = new SwingEntityModel(new CityTableModel(connectionProvider));
    SwingEntityModel countryLanguageModel = new SwingEntityModel(new CountryLanguageTableModel(connectionProvider));
    addDetailModels(cityModel, countryLanguageModel);
  }
}
CountryEditModel
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.value.Value;
import is.codion.common.value.ValueObserver;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.component.EntityComboBoxModel;

public final class CountryEditModel extends SwingEntityEditModel {

  private final Value<Double> averageCityPopulation = Value.value();

  CountryEditModel(EntityConnectionProvider connectionProvider) {
    super(Country.TYPE, connectionProvider);
    initializeComboBoxModels(Country.CAPITAL_FK);
    addEntityListener(country -> averageCityPopulation.set(averageCityPopulation(country)));
  }

  @Override
  public EntityComboBoxModel createForeignKeyComboBoxModel(ForeignKey foreignKey) {
    EntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKey);
    if (foreignKey.equals(Country.CAPITAL_FK)) {
      //only show cities for currently selected country
      addEntityListener(country ->
              comboBoxModel.includeCondition().set(cityEntity ->
                      cityEntity.castTo(City.class)
                              .isInCountry(country)));
    }

    return comboBoxModel;
  }

  public ValueObserver<Double> averageCityPopulation() {
    return averageCityPopulation.observer();
  }

  private Double averageCityPopulation(Entity country) {
    if (country == null) {
      return null;
    }
    try {
      return connectionProvider().connection().execute(Country.AVERAGE_CITY_POPULATION, country.get(Country.CODE));
    }
    catch (DatabaseException e) {
      throw new RuntimeException(e);
    }
  }
}
CountryTableModel
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.report.ReportException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.condition.Condition;
import is.codion.framework.model.EntitySearchConditionModel;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.framework.model.SwingEntityTableModel;

import net.sf.jasperreports.engine.JasperPrint;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

import static is.codion.plugin.jasperreports.JasperReports.classPathReport;
import static is.codion.plugin.jasperreports.JasperReports.fillReport;
import static java.util.Collections.singletonMap;

public final class CountryTableModel extends SwingEntityTableModel {

  private static final String CITY_SUBREPORT_PARAMETER = "CITY_SUBREPORT";
  private static final String COUNTRY_REPORT = "country_report.jasper";
  private static final String CITY_REPORT = "city_report.jasper";

  CountryTableModel(EntityConnectionProvider connectionProvider) {
    super(new CountryEditModel(connectionProvider));
    configureCapitalConditionModel();
  }

  public JasperPrint fillCountryReport(ProgressReporter<String> progressReporter) throws ReportException {
    CountryReportDataSource dataSource = new CountryReportDataSource(selectionModel().getSelectedItems(),
            connectionProvider().connection(), progressReporter);

    return fillReport(classPathReport(CountryTableModel.class, COUNTRY_REPORT), dataSource, reportParameters());
  }

  private static Map<String, Object> reportParameters() throws ReportException {
    return new HashMap<>(singletonMap(CITY_SUBREPORT_PARAMETER,
            classPathReport(CityTableModel.class, CITY_REPORT).load()));
  }

  private void configureCapitalConditionModel() {
    ((EntitySearchConditionModel) conditionModel()
            .attributeModel(Country.CAPITAL_FK))
            .searchModel()
            .condition()
            .set(new CapitalConditionSupplier());
  }

  private final class CapitalConditionSupplier implements Supplier<Condition> {
    @Override
    public Condition get() {
      EntityConnection connection = connectionProvider().connection();
      try {
        return City.ID.in(connection.select(Country.CAPITAL));
      }
      catch (DatabaseException e) {
        throw new RuntimeException(e);
      }
    }
  }
}
country_report.jrxml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.18.1.final using JasperReports Library version 6.18.1-9d75d1969e774d4f179fb3be8401e98a0e6d1611  -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="country_report" pageWidth="595" pageHeight="842" columnWidth="535" leftMargin="30" rightMargin="30" topMargin="20" bottomMargin="20" uuid="65647229-b9ea-492c-9579-df6c5298a9a2">
	<property name="ireport.scriptlethandling" value="2"/>
	<property name="ireport.encoding" value="UTF-8"/>
	<property name="ireport.zoom" value="1.0"/>
	<property name="ireport.x" value="0"/>
	<property name="ireport.y" value="0"/>
	<import value="is.codion.framework.demos.world.model.CountryReportDataSource"/>
	<parameter name="CITY_SUBREPORT" class="net.sf.jasperreports.engine.JasperReport"/>
	<field name="name" class="java.lang.String"/>
	<field name="continent" class="java.lang.String"/>
	<field name="region" class="java.lang.String"/>
	<field name="surfacearea" class="java.lang.Double"/>
	<field name="population" class="java.lang.Integer"/>
	<background>
		<band splitType="Stretch"/>
	</background>
	<detail>
		<band height="52">
			<property name="com.jaspersoft.studio.unit.height" value="px"/>
			<textField>
				<reportElement x="0" y="14" width="100" height="14" uuid="52591026-2ffa-4ca3-b73d-31bb53463220"/>
				<textFieldExpression><![CDATA[$F{name}]]></textFieldExpression>
			</textField>
			<textField>
				<reportElement x="100" y="14" width="100" height="14" uuid="a37319e9-d3d7-4adc-a058-4783281221be">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<textFieldExpression><![CDATA[$F{continent}]]></textFieldExpression>
			</textField>
			<textField>
				<reportElement x="200" y="14" width="140" height="14" uuid="94bb3563-d458-47f3-bb98-bb9914e458f7"/>
				<textFieldExpression><![CDATA[$F{region}]]></textFieldExpression>
			</textField>
			<textField pattern="###,###,###,###">
				<reportElement x="350" y="14" width="84" height="14" uuid="7ae9640c-d57f-4cd9-b08c-b2e582ae4084">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<textElement textAlignment="Center"/>
				<textFieldExpression><![CDATA[$F{surfacearea}]]></textFieldExpression>
			</textField>
			<textField pattern="###,###,###,###">
				<reportElement x="435" y="14" width="100" height="14" uuid="0aeaac02-df9f-4c84-9533-3daee90d0082">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<textElement textAlignment="Center"/>
				<textFieldExpression><![CDATA[$F{population}]]></textFieldExpression>
			</textField>
			<subreport>
				<reportElement x="-30" y="30" width="595" height="16" uuid="fe0b73e2-e164-4dd6-a08d-9afb8d964fc2">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<dataSourceExpression><![CDATA[((CountryReportDataSource) $P{REPORT_DATA_SOURCE}).cityDataSource()]]></dataSourceExpression>
				<subreportExpression><![CDATA[$P{CITY_SUBREPORT}]]></subreportExpression>
			</subreport>
			<staticText>
				<reportElement x="0" y="0" width="100" height="14" uuid="c61d049d-040d-4aba-9cf4-4aa277459142"/>
				<textElement>
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Name]]></text>
			</staticText>
			<staticText>
				<reportElement x="100" y="0" width="100" height="14" uuid="2fcd930a-a99f-4f90-b303-4b33938c1a34">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<textElement>
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Continent]]></text>
			</staticText>
			<staticText>
				<reportElement x="200" y="0" width="140" height="14" uuid="510dab14-feac-4935-b8b6-130686c249dc"/>
				<textElement>
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Region]]></text>
			</staticText>
			<staticText>
				<reportElement x="350" y="0" width="84" height="14" uuid="2ee9691c-9b08-44ea-a57f-fc3e58608a95">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<textElement textAlignment="Center">
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Surface area]]></text>
			</staticText>
			<staticText>
				<reportElement x="435" y="0" width="100" height="14" uuid="ac3794a2-c395-49ea-848f-066599941669">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<textElement textAlignment="Center">
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Population]]></text>
			</staticText>
		</band>
	</detail>
</jasperReport>
city_report.jrxml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.18.1.final using JasperReports Library version 6.18.1-9d75d1969e774d4f179fb3be8401e98a0e6d1611  -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="city_report" pageWidth="595" pageHeight="842" columnWidth="535" leftMargin="30" rightMargin="30" topMargin="0" bottomMargin="0" uuid="65647229-b9ea-492c-9579-df6c5298a9a2">
	<property name="ireport.scriptlethandling" value="2"/>
	<property name="ireport.encoding" value="UTF-8"/>
	<property name="ireport.zoom" value="1.0"/>
	<property name="ireport.x" value="0"/>
	<property name="ireport.y" value="0"/>
	<property name="com.jaspersoft.studio.unit." value="pixel"/>
	<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
	<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
	<field name="name" class="java.lang.String"/>
	<field name="population" class="java.lang.Integer"/>
	<background>
		<band splitType="Stretch"/>
	</background>
	<columnHeader>
		<band height="16">
			<property name="com.jaspersoft.studio.unit.height" value="px"/>
			<staticText>
				<reportElement x="350" y="2" width="84" height="14" uuid="ef712d38-e1c6-4922-8e38-4bd263230db5"/>
				<textElement>
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Name]]></text>
			</staticText>
			<staticText>
				<reportElement x="434" y="2" width="100" height="14" uuid="8c05be12-bf23-4fac-ac18-47a1e18f5959"/>
				<textElement textAlignment="Center">
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Population]]></text>
			</staticText>
			<staticText>
				<reportElement x="250" y="2" width="100" height="14" uuid="44915dc4-f2b8-4661-9b9a-8cb6e6f91f58"/>
				<textElement>
					<font isBold="true"/>
				</textElement>
				<text><![CDATA[Largest Cities]]></text>
			</staticText>
			<line>
				<reportElement x="0" y="1" width="535" height="1" uuid="468587c6-550e-4dc1-80c2-72074816c6ff">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<graphicElement>
					<pen lineStyle="Dotted"/>
				</graphicElement>
			</line>
		</band>
	</columnHeader>
	<detail>
		<band height="14">
			<property name="com.jaspersoft.studio.unit.height" value="px"/>
			<textField>
				<reportElement x="350" y="0" width="84" height="14" uuid="52591026-2ffa-4ca3-b73d-31bb53463220"/>
				<textFieldExpression><![CDATA[$F{name}]]></textFieldExpression>
			</textField>
			<textField pattern="###,###,###,###">
				<reportElement x="434" y="0" width="100" height="14" uuid="376a08f3-5e56-45cd-974e-3635b8248373"/>
				<textElement textAlignment="Center"/>
				<textFieldExpression><![CDATA[$F{population}]]></textFieldExpression>
			</textField>
		</band>
	</detail>
	<columnFooter>
		<band height="1">
			<property name="com.jaspersoft.studio.unit.height" value="px"/>
			<line>
				<reportElement x="0" y="0" width="535" height="1" uuid="e0b89b1f-85c9-4dd3-a316-a4a8359db499">
					<property name="com.jaspersoft.studio.unit.height" value="px"/>
				</reportElement>
				<graphicElement>
					<pen lineStyle="Dotted"/>
				</graphicElement>
			</line>
		</band>
	</columnFooter>
</jasperReport>
CountryReportDataSource
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.plugin.jasperreports.JasperReportsDataSource;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRField;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import static is.codion.framework.db.EntityConnection.Select.where;
import static is.codion.framework.domain.entity.OrderBy.descending;

public final class CountryReportDataSource extends JasperReportsDataSource<Country> {

  private final EntityConnection connection;

  CountryReportDataSource(List<Entity> countries, EntityConnection connection,
                          ProgressReporter<String> progressReporter) {
    super(Entity.castTo(Country.class, countries).iterator(), new CountryValueProvider(),
            new CountryReportProgressReporter(progressReporter));
    this.connection = connection;
  }

  /* See usage in src/main/reports/country_report.jrxml, subreport element */
  public JRDataSource cityDataSource() {
    Country country = currentItem();
    try {
      Collection<City> largestCities = Entity.castTo(City.class,
              connection.select(where(City.COUNTRY_FK.equalTo(country))
                      .attributes(City.NAME, City.POPULATION)
                      .orderBy(descending(City.POPULATION))
                      .limit(5)
                      .build()));

      return new JasperReportsDataSource<>(largestCities.iterator(), new CityValueProvider());
    }
    catch (DatabaseException e) {
      throw new RuntimeException(e);
    }
  }

  private static final class CountryValueProvider implements BiFunction<Country, JRField, Object> {

    private static final String NAME = "name";
    private static final String CONTINENT = "continent";
    private static final String REGION = "region";
    private static final String SURFACEAREA = "surfacearea";
    private static final String POPULATION = "population";

    @Override
    public Object apply(Country country, JRField field) {
      return switch (field.getName()) {
        case NAME -> country.name();
        case CONTINENT -> country.continent();
        case REGION -> country.region();
        case SURFACEAREA -> country.surfacearea();
        case POPULATION -> country.population();
        default -> throw new IllegalArgumentException("Unknown field: " + field.getName());
      };
    }
  }

  private static final class CityValueProvider implements BiFunction<City, JRField, Object> {

    private static final String NAME = "name";
    private static final String POPULATION = "population";

    @Override
    public Object apply(City city, JRField field) {
      return switch (field.getName()) {
        case NAME -> city.name();
        case POPULATION -> city.population();
        default -> throw new IllegalArgumentException("Unknown field: " + field.getName());
      };
    }
  }

  private static final class CountryReportProgressReporter implements Consumer<Country> {

    private final AtomicInteger counter = new AtomicInteger();
    private final ProgressReporter<String> progressReporter;

    private CountryReportProgressReporter(ProgressReporter<String> progressReporter) {
      this.progressReporter = progressReporter;
    }

    @Override
    public void accept(Country country) {
      progressReporter.publish(country.name());
      progressReporter.report(counter.incrementAndGet());
    }
  }
}
CountryReportDataSourceTest
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.user.User;
import is.codion.common.value.Value;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.demos.world.domain.WorldImpl;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.base.JRBaseField;
import org.junit.jupiter.api.Test;

import java.util.List;

import static is.codion.framework.db.EntityConnection.Select.where;
import static is.codion.framework.domain.entity.OrderBy.ascending;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public final class CountryReportDataSourceTest {

  private static final User UNIT_TEST_USER =
          User.parse(System.getProperty("codion.test.user", "scott:tiger"));

  @Test
  void iterate() throws DatabaseException, JRException {
    try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
      Value<Integer> progressCounter = Value.value();
      Value<String> publishedValue = Value.value();
      ProgressReporter<String> progressReporter = new ProgressReporter<>() {
        @Override
        public void report(int progress) {
          progressCounter.set(progress);
        }

        @Override
        public void publish(String... chunks) {
          publishedValue.set(chunks[0]);
        }
      };

      EntityConnection connection = connectionProvider.connection();
      List<Entity> countries =
              connection.select(where(Country.NAME.in("Denmark", "Iceland"))
              .orderBy(ascending(Country.NAME))
              .build());
      CountryReportDataSource countryReportDataSource = new CountryReportDataSource(countries, connection, progressReporter);
      assertThrows(IllegalStateException.class, countryReportDataSource::cityDataSource);

      countryReportDataSource.next();
      assertEquals("Denmark", countryReportDataSource.getFieldValue(field(Country.NAME)));
      assertEquals("Europe", countryReportDataSource.getFieldValue(field(Country.CONTINENT)));
      assertEquals("Nordic Countries", countryReportDataSource.getFieldValue(field(Country.REGION)));
      assertEquals(43094d, countryReportDataSource.getFieldValue(field(Country.SURFACEAREA)));
      assertEquals(5330000, countryReportDataSource.getFieldValue(field(Country.POPULATION)));
      assertThrows(IllegalArgumentException.class, () -> countryReportDataSource.getFieldValue(field(City.LOCATION)));

      JRDataSource denmarkCityDataSource = countryReportDataSource.cityDataSource();
      denmarkCityDataSource.next();
      assertEquals("K\u00F8benhavn", denmarkCityDataSource.getFieldValue(field(City.NAME)));
      assertEquals(495699, denmarkCityDataSource.getFieldValue(field(City.POPULATION)));
      assertThrows(IllegalArgumentException.class, () -> denmarkCityDataSource.getFieldValue(field(Country.REGION)));
      denmarkCityDataSource.next();
      assertEquals("\u00C5rhus", denmarkCityDataSource.getFieldValue(field(City.NAME)));

      countryReportDataSource.next();
      assertEquals("Iceland", countryReportDataSource.getFieldValue(field(Country.NAME)));

      JRDataSource icelandCityDataSource = countryReportDataSource.cityDataSource();
      icelandCityDataSource.next();
      assertEquals("Reykjav\u00EDk", icelandCityDataSource.getFieldValue(field(City.NAME)));

      assertEquals(2, progressCounter.get());
      assertEquals("Iceland", publishedValue.get());
    }
  }

  private static EntityConnectionProvider createConnectionProvider() {
    return LocalEntityConnectionProvider.builder()
            .domain(new WorldImpl())
            .user(UNIT_TEST_USER)
            .build();
  }

  private static JRField field(Attribute<?> attribute) {
    return new TestField(attribute.name());
  }

  private static final class TestField extends JRBaseField {

    private TestField(String name) {
      this.name = name;
    }
  }
}

UI

CountryPanel
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.demos.world.model.CountryModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityPanel;

import static is.codion.swing.framework.ui.TabbedPanelLayout.splitPaneResizeWeight;

final class CountryPanel extends EntityPanel {

  CountryPanel(CountryModel countryModel) {
    super(countryModel,
            new CountryEditPanel(countryModel.editModel()),
            new CountryTablePanel(countryModel.tableModel()),
            splitPaneResizeWeight(0.7));

    SwingEntityModel cityModel = countryModel.detailModel(City.TYPE);
    EntityPanel cityPanel = new EntityPanel(cityModel,
            new CityEditPanel(cityModel.tableModel()),
            new CityTablePanel(cityModel.tableModel()));

    SwingEntityModel countryLanguageModel = countryModel.detailModel(CountryLanguage.TYPE);
    EntityPanel countryLanguagePanel = new EntityPanel(countryLanguageModel,
            new CountryLanguageEditPanel(countryLanguageModel.editModel()),
            new CountryLanguageTablePanel(countryLanguageModel.tableModel()));

    addDetailPanels(cityPanel, countryLanguagePanel);
  }
}
CountryEditPanel
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.model.CountryEditModel;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.ui.component.text.NumberField;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

import static is.codion.swing.common.ui.component.Components.*;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

final class CountryEditPanel extends EntityEditPanel {

  private static final int PREFERRED_COMBO_BOX_WIDTH = 120;

  CountryEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Country.CODE);

    createTextField(Country.CODE)
            .columns(6)
            .upperCase(true);
    createTextField(Country.CODE_2)
            .columns(6)
            .upperCase(true);
    createTextField(Country.NAME);
    createTextField(Country.LOCALNAME);
    createItemComboBox(Country.CONTINENT)
            .preferredWidth(PREFERRED_COMBO_BOX_WIDTH);
    createComboBox(Country.REGION)
            .preferredWidth(PREFERRED_COMBO_BOX_WIDTH);
    createTextField(Country.SURFACEAREA)
            .columns(5);
    createTextField(Country.INDEPYEAR)
            .columns(5);
    createTextField(Country.POPULATION)
            .columns(5);
    createTextField(Country.LIFE_EXPECTANCY)
            .columns(5);
    createTextField(Country.GNP)
            .columns(6);
    createTextField(Country.GNPOLD)
            .columns(6);
    createComboBox(Country.GOVERNMENTFORM)
            .preferredWidth(PREFERRED_COMBO_BOX_WIDTH)
            .editable(true);
    createTextField(Country.HEADOFSTATE);
    //create a panel with a button for adding a new city
    createForeignKeyComboBoxPanel(Country.CAPITAL_FK, this::createCapitalEditPanel)
            .add(true);
    //add a field displaying the avarage city population for the selected country
    CountryEditModel editModel = editModel();
    NumberField<Double> averageCityPopulationField = doubleField()
            .linkedValue(editModel.averageCityPopulation())
            .maximumFractionDigits(2)
            .groupingUsed(true)
            .horizontalAlignment(SwingConstants.CENTER)
            .focusable(false)
            .editable(false)
            .build();

    JPanel codePanel = gridLayoutPanel(1, 2)
            .add(createInputPanel(Country.CODE))
            .add(createInputPanel(Country.CODE_2))
            .build();

    JPanel gnpPanel = gridLayoutPanel(1, 2)
            .add(createInputPanel(Country.GNP))
            .add(createInputPanel(Country.GNPOLD))
            .build();

    JPanel surfaceAreaIndYearPanel = gridLayoutPanel(1, 2)
            .add(createInputPanel(Country.SURFACEAREA))
            .add(createInputPanel(Country.INDEPYEAR))
            .build();

    JPanel populationLifeExpectancyPanel = gridLayoutPanel(1, 2)
            .add(createInputPanel(Country.POPULATION))
            .add(createInputPanel(Country.LIFE_EXPECTANCY))
            .build();

    setLayout(gridLayout(4, 5));

    add(codePanel);
    addInputPanel(Country.NAME);
    addInputPanel(Country.LOCALNAME);
    addInputPanel(Country.CAPITAL_FK);
    addInputPanel(Country.CONTINENT);
    addInputPanel(Country.REGION);
    add(surfaceAreaIndYearPanel);
    add(populationLifeExpectancyPanel);
    add(gnpPanel);
    addInputPanel(Country.GOVERNMENTFORM);
    addInputPanel(Country.HEADOFSTATE);
    add(createInputPanel(label("Avg. city population")
            .horizontalAlignment(SwingConstants.CENTER)
            .build(), averageCityPopulationField));
  }

  private EntityEditPanel createCapitalEditPanel() {
    CityEditPanel capitalEditPanel = new CityEditPanel(new SwingEntityEditModel(City.TYPE, editModel().connectionProvider()));
    Entity country = editModel().entity();
    if (country.primaryKey().isNotNull()) {
      //if a country is selected, then we don't allow it to be changed
      capitalEditPanel.editModel().put(City.COUNTRY_FK, country);
      //initialize the panel components, so we can configure the country component
      capitalEditPanel.initialize();
      //disable the country selection component
      JComponent countryComponent = capitalEditPanel.component(City.COUNTRY_FK).get();
      countryComponent.setEnabled(false);
      countryComponent.setFocusable(false);
      //and change the initial focus property
      capitalEditPanel.initialFocusAttribute().set(City.NAME);
    }

    return capitalEditPanel;
  }
}
CountryTablePanel
package is.codion.framework.demos.world.ui;

import is.codion.common.db.report.ReportException;
import is.codion.framework.demos.world.domain.api.World;
import is.codion.framework.demos.world.model.CountryTableModel;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import is.codion.swing.framework.ui.icon.FrameworkIcons;

import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.swing.JRViewer;

import java.awt.Dimension;

final class CountryTablePanel extends EntityTablePanel {

  CountryTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
    configure().editableAttributes().remove(World.Country.CAPITAL_FK);
  }

  @Override
  protected void setupControls() {
    control(TableControl.PRINT).set(Control.builder(this::viewCountryReport)
            .name("Country report")
            .enabled(tableModel().selectionModel().selectionNotEmpty())
            .smallIcon(FrameworkIcons.instance().print())
            .build());
  }

  private void viewCountryReport() {
    Dialogs.progressWorkerDialog(this::fillCountryReport)
            .owner(this)
            .maximumProgress(tableModel().selectionModel().selectionCount())
            .stringPainted(true)
            .onResult(this::viewReport)
            .execute();
  }

  private JasperPrint fillCountryReport(ProgressReporter<String> progressReporter) throws ReportException {
    CountryTableModel countryTableModel = tableModel();

    return countryTableModel.fillCountryReport(progressReporter);
  }

  private void viewReport(JasperPrint countryReport) {
    Dialogs.componentDialog(new JRViewer(countryReport))
            .owner(this)
            .modal(false)
            .title("Country report")
            .size(new Dimension(800, 600))
            .show();
  }
}

City

SQL

create table world.city (
  id int not null,
  name varchar(35) not null,
  countrycode varchar(3) not null,
  district varchar(20) not null,
  population int not null,
  location geometry(point),
  constraint city_pk primary key (id)
);

Domain

API
  interface City extends Entity {
    EntityType TYPE = DOMAIN.entityType("world.city", City.class);

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

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

    String name();
    int population();
    Country country();
    void location(Location location);

    default boolean isInCountry(Entity country) {
      return country != null && Objects.equals(country(), country);
    }

    default boolean isCapital() {
      return Objects.equals(get(City.ID), country().get(Country.CAPITAL));
    }
  }
  record Location(double latitude, double longitude) implements Serializable {

    @Override
    public String toString() {
      return "[" + latitude + "," + longitude + "]";
    }
  }
  final class LocationComparator implements Comparator<Location>, Serializable {

    @Override
    public int compare(Location l1, Location l2) {
      int result = Double.compare(l1.latitude(), l2.latitude());
      if (result == 0) {
        return Double.compare(l1.longitude(), l2.longitude());
      }

      return result;
    }
  }
  final class CityValidator extends DefaultEntityValidator implements Serializable {

    @Serial
    private static final long serialVersionUID = 1;

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

    @Serial
    private static final long serialVersionUID = 1;

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

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

      return null;
    }
  }
Implementation
  void city() {
    add(City.TYPE.define(
            City.ID.define()
                    .primaryKey(),
            City.NAME.define()
                    .column()
                    .caption("Name")
                    .searchable(true)
                    .nullable(false)
                    .maximumLength(35),
            City.COUNTRY_CODE.define()
                    .column()
                    .nullable(false),
            City.COUNTRY_FK.define()
                    .foreignKey()
                    .caption("Country"),
            City.DISTRICT.define()
                    .column()
                    .caption("District")
                    .nullable(false)
                    .maximumLength(20),
            City.POPULATION.define()
                    .column()
                    .caption("Population")
                    .nullable(false)
                    .numberFormatGrouping(true),
            City.LOCATION.define()
                    .column()
                    .caption("Location")
                    .columnClass(String.class, new LocationConverter())
                    .comparator(new LocationComparator()))
            .keyGenerator(sequence("world.city_seq"))
            .validator(new CityValidator())
            .orderBy(ascending(City.NAME))
            .stringFactory(City.NAME)
            .foregroundColorProvider(new CityColorProvider())
            .caption("City"));
  }
  private static final class LocationConverter implements Converter<Location, String> {

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

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

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

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

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

Model

CityEditModel
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Location;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.swing.framework.model.SwingEntityEditModel;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Optional;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.joining;

public final class CityEditModel extends SwingEntityEditModel {

  private static final String OPENSTREETMAP_ORG_SEARCH = "https://nominatim.openstreetmap.org/search?q=";

  public CityEditModel(EntityConnectionProvider connectionProvider) {
    super(City.TYPE, connectionProvider);
    initializeComboBoxModels(City.COUNTRY_FK);
  }

  public void populateLocation() throws IOException, DatabaseException, ValidationException {
    Location location = lookupLocation(entity().castTo(City.class))
            .orElseThrow(() -> new RuntimeException("Location not found for city: " + entity()));
    put(City.LOCATION, location);
    if (modified().get()) {
      update();
    }
  }

  void populateLocation(City city) throws IOException, DatabaseException, ValidationException {
    lookupLocation(city).ifPresent(city::location);
    if (city.modified()) {
      update(singletonList(city));
    }
  }

  private static Optional<Location> lookupLocation(City city) throws IOException {
    JSONArray jsonArray = toJSONArray(new URL(OPENSTREETMAP_ORG_SEARCH +
            URLEncoder.encode(city.name(), UTF_8) + "," +
            URLEncoder.encode(city.country().name(), UTF_8) + "&format=json"));
    if (!jsonArray.isEmpty()) {
      JSONObject cityInformation = (JSONObject) jsonArray.get(0);

      return Optional.of(new Location(cityInformation.getDouble("lat"), cityInformation.getDouble("lon")));
    }

    return Optional.empty();
  }

  private static JSONArray toJSONArray(URL url) throws IOException {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream(), UTF_8))) {
      return new JSONArray(reader.lines().collect(joining()));
    }
  }
}
CityTableModel
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.event.Event;
import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.framework.model.SwingEntityTableModel;

import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.Consumer;

import static java.util.Collections.singletonList;

public final class CityTableModel extends SwingEntityTableModel {

  private final DefaultPieDataset<String> chartDataset = new DefaultPieDataset<>();
  private final Event<Collection<Entity>> displayLocationEvent = Event.event();
  private final State citiesWithoutLocationSelected = State.state();

  CityTableModel(EntityConnectionProvider connectionProvider) {
    super(new CityEditModel(connectionProvider));
    selectionModel().addSelectedItemsListener(displayLocationEvent::accept);
    selectionModel().addSelectionListener(this::updateCitiesWithoutLocationSelected);
    refresher().addRefreshListener(this::refreshChartDataset);
  }

  public PieDataset<String> chartDataset() {
    return chartDataset;
  }

  public void addDisplayLocationListener(Consumer<Collection<Entity>> listener) {
    displayLocationEvent.addDataListener(listener);
  }

  public StateObserver citiesWithoutLocationSelected() {
    return citiesWithoutLocationSelected.observer();
  }

  public void populateLocationForSelected(ProgressReporter<String> progressReporter,
                                          StateObserver cancelPopulateLocation)
          throws IOException, DatabaseException, ValidationException {
    Collection<Entity> updatedCities = new ArrayList<>();
    Collection<City> selectedCitiesWithoutLocation = selectionModel().getSelectedItems().stream()
            .filter(city -> city.isNull(City.LOCATION))
            .map(city -> city.castTo(City.class))
            .toList();
    CityEditModel editModel = editModel();
    Iterator<City> citiesWithoutLocation = selectedCitiesWithoutLocation.iterator();
    while (citiesWithoutLocation.hasNext() && !cancelPopulateLocation.get()) {
      City city = citiesWithoutLocation.next();
      progressReporter.publish(city.country().name() + " - " + city.name());
      editModel.populateLocation(city);
      updatedCities.add(city);
      progressReporter.report(100 * updatedCities.size() / selectedCitiesWithoutLocation.size());
      displayLocationEvent.accept(singletonList(city));
    }
    displayLocationEvent.accept(selectionModel().getSelectedItems());
  }

  private void refreshChartDataset() {
    chartDataset.clear();
    Entity.castTo(City.class, visibleItems()).forEach(city ->
            chartDataset.setValue(city.name(), city.population()));
  }

  private void updateCitiesWithoutLocationSelected() {
    citiesWithoutLocationSelected.set(selectionModel().getSelectedItems().stream()
            .anyMatch(city -> city.isNull(City.LOCATION)));
  }
}

UI

CityEditPanel
package is.codion.framework.demos.world.ui;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.state.State;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.model.CityEditModel;
import is.codion.framework.demos.world.model.CityTableModel;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.icon.FrameworkIcons;

import org.jxmapviewer.JXMapKit;
import org.kordamp.ikonli.foundation.Foundation;

import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.io.IOException;
import java.util.Collection;

import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toSet;

public final class CityEditPanel extends EntityEditPanel {

  private final JXMapKit mapKit;

  public CityEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
    this.mapKit = null;
  }

  CityEditPanel(CityTableModel tableModel) {
    super(tableModel.editModel());
    this.mapKit = Maps.createMapKit();
    tableModel.addDisplayLocationListener(this::displayLocation);
  }

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(City.COUNTRY_FK);

    createForeignKeyComboBox(City.COUNTRY_FK)
            .preferredWidth(120);
    createTextField(City.NAME);
    createTextField(City.DISTRICT);
    createTextField(City.POPULATION);

    JPanel inputPanel = gridLayoutPanel(0, 1)
            .add(createInputPanel(City.COUNTRY_FK))
            .add(createInputPanel(City.NAME))
            .add(createInputPanel(City.DISTRICT))
            .add(createInputPanel(City.POPULATION))
            .build();

    JPanel centerPanel = gridLayoutPanel(1, 0)
            .add(inputPanel)
            .build();
    if (mapKit != null) {
      centerPanel.add(mapKit);
    }
    setLayout(borderLayout());
    add(centerPanel, BorderLayout.CENTER);
  }

  @Override
  protected Controls createControls() {
    return super.createControls()
            .addAt(4, Control.builder(this::populateLocation)
                    .enabled(State.and(active(),
                            editModel().isNull(City.LOCATION),
                            editModel().exists()))
                    .smallIcon(FrameworkIcons.instance().icon(Foundation.MAP))
                    .build());
  }

  private void populateLocation() throws ValidationException, IOException, DatabaseException {
    CityEditModel editModel = editModel();
    editModel.populateLocation();
    displayLocation(singleton(editModel.entity()));
  }

  private void displayLocation(Collection<Entity> cities) {
    Maps.paintWaypoints(cities.stream()
            .filter(city -> city.isNotNull(City.LOCATION))
            .map(city -> city.get(City.LOCATION))
            .collect(toSet()), mapKit.getMainMap());
  }
}
CityTablePanel
package is.codion.framework.demos.world.ui;

import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.model.CityTableModel;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressTask;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.ui.icon.FrameworkIcons;

import org.kordamp.ikonli.foundation.Foundation;

import java.util.List;

final class CityTablePanel extends ChartTablePanel {

  CityTablePanel(CityTableModel tableModel) {
    super(tableModel, tableModel.chartDataset(), "Cities");
    configure().editableAttributes().remove(City.LOCATION);
  }

  @Override
  protected Controls createPopupMenuControls(List<Controls> additionalPopupMenuControls) {
    return super.createPopupMenuControls(additionalPopupMenuControls)
            .addAt(0, createPopulateLocationControl())
            .addSeparatorAt(1);
  }

  private Control createPopulateLocationControl() {
    CityTableModel cityTableModel = tableModel();

    return Control.builder(this::populateLocation)
            .name("Populate location")
            .enabled(cityTableModel.citiesWithoutLocationSelected())
            .smallIcon(FrameworkIcons.instance().icon(Foundation.MAP))
            .build();
  }

  private void populateLocation() {
    PopulateLocationTask populateLocationTask = new PopulateLocationTask(tableModel());

    Dialogs.progressWorkerDialog(populateLocationTask)
            .owner(this)
            .title("Populating locations")
            .stringPainted(true)
            .controls(Controls.builder()
                    .control(Control.builder(populateLocationTask::cancel)
                            .name("Cancel")
                            .enabled(populateLocationTask.isWorking()))
                    .build())
            .onException(this::displayPopulateException)
            .execute();
  }

  private void displayPopulateException(Throwable exception) {
    Dialogs.exceptionDialog()
            .owner(this)
            .title("Unable to populate location")
            .show(exception);
  }

  private static final class PopulateLocationTask implements ProgressTask<Void, String> {

    private final CityTableModel tableModel;
    private final State cancelled = State.state();

    private PopulateLocationTask(CityTableModel tableModel) {
      this.tableModel = tableModel;
    }

    @Override
    public Void execute(ProgressReporter<String> progressReporter) throws Exception {
      tableModel.populateLocationForSelected(progressReporter, cancelled);
      return null;
    }

    private void cancel() {
      cancelled.set(true);
    }

    private StateObserver isWorking() {
      return cancelled.not();
    }
  }
}

CountryLanguage

SQL

create table world.countrylanguage (
  countrycode varchar(3) not null,
  language varchar(30) not null,
  isofficial boolean default false not null,
  percentage decimal(4,1) not null,
  constraint countrylanguage_pk primary key (countrycode, language),
  constraint countrylanguage_country_fk foreign key (countrycode) references world.country(code)
);

Domain

API
  interface CountryLanguage extends Entity {
    EntityType TYPE = DOMAIN.entityType("world.countrylanguage", CountryLanguage.class);

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

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

    String language();
    int noOfSpeakers();
  }
  final class NoOfSpeakersProvider implements DerivedAttribute.Provider<Integer> {

    @Serial
    private static final long serialVersionUID = 1;

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

      return null;
    }
  }
Implementation
  void countryLanguage() {
    add(CountryLanguage.TYPE.define(
            CountryLanguage.COUNTRY_CODE.define()
                    .primaryKey(0)
                    .updatable(true),
            CountryLanguage.LANGUAGE.define()
                    .primaryKey(1)
                    .caption("Language")
                    .updatable(true),
            CountryLanguage.COUNTRY_FK.define()
                    .foreignKey()
                    .caption("Country"),
            CountryLanguage.IS_OFFICIAL.define()
                    .column()
                    .caption("Is official")
                    .columnHasDefaultValue(true)
                    .nullable(false),
            CountryLanguage.NO_OF_SPEAKERS.define()
                    .derived(new NoOfSpeakersProvider(),
                            CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
                    .caption("No. of speakers")
                    .numberFormatGrouping(true),
            CountryLanguage.PERCENTAGE.define()
                    .column()
                    .caption("Percentage")
                    .nullable(false)
                    .maximumFractionDigits(1)
                    .valueRange(0, 100))
            .orderBy(OrderBy.builder()
                    .ascending(CountryLanguage.LANGUAGE)
                    .descending(CountryLanguage.PERCENTAGE)
                    .build())
            .caption("Language"));
  }

Model

CountryLanguageTableModel
package is.codion.framework.demos.world.model;

import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingEntityTableModel;

import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;

public final class CountryLanguageTableModel extends SwingEntityTableModel {

  private final DefaultPieDataset<String> chartDataset = new DefaultPieDataset<>();

  CountryLanguageTableModel(EntityConnectionProvider connectionProvider) {
    super(CountryLanguage.TYPE, connectionProvider);
    editModel().initializeComboBoxModels(CountryLanguage.COUNTRY_FK);
    refresher().addRefreshListener(this::refreshChartDataset);
  }

  public PieDataset<String> chartDataset() {
    return chartDataset;
  }

  private void refreshChartDataset() {
    chartDataset.clear();
    Entity.castTo(CountryLanguage.class, visibleItems()).forEach(language ->
            chartDataset.setValue(language.language(), language.noOfSpeakers()));
  }
}

UI

CountryLanguageEditPanel
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import javax.swing.JPanel;

import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

final class CountryLanguageEditPanel extends EntityEditPanel {

  CountryLanguageEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(CountryLanguage.COUNTRY_FK);

    createForeignKeyComboBox(CountryLanguage.COUNTRY_FK)
            .preferredWidth(120);
    createTextField(CountryLanguage.LANGUAGE);
    createCheckBox(CountryLanguage.IS_OFFICIAL);
    createTextField(CountryLanguage.PERCENTAGE)
            .columns(4);

    JPanel percentageOfficialPanel = gridLayoutPanel(1, 2)
            .add(createInputPanel(CountryLanguage.PERCENTAGE))
            .add(createInputPanel(CountryLanguage.IS_OFFICIAL))
            .build();

    setLayout(gridLayout(0, 1));

    addInputPanel(CountryLanguage.COUNTRY_FK);
    addInputPanel(CountryLanguage.LANGUAGE);
    add(percentageOfficialPanel);
  }
}
CountryLanguageTablePanel
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.model.CountryLanguageTableModel;

final class CountryLanguageTablePanel extends ChartTablePanel {

  CountryLanguageTablePanel(CountryLanguageTableModel tableModel) {
    super(tableModel, tableModel.chartDataset(), "Languages");
  }
}

Continents

Continent

Domain

API
  interface Continent extends Entity {
    EntityType TYPE = DOMAIN.entityType("continent", Continent.class);

    Column<String> NAME = TYPE.stringColumn("continent");
    Column<Integer> SURFACE_AREA = TYPE.integerColumn("surface_area");
    Column<Long> POPULATION = TYPE.longColumn("population");
    Column<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleColumn("min_life_expectancy");
    Column<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleColumn("max_life_expectancy");
    Column<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerColumn("min_indep_year");
    Column<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerColumn("max_indep_year");
    Column<Double> GNP = TYPE.doubleColumn("gnp");

    String name();
    int surfaceArea();
    long population();
    double minLifeExpectancy();
    double maxLifeExpectancy();
    double gnp();
  }
Implementation
  void continent() {
    add(Continent.TYPE.define(
            Continent.NAME.define()
                    .column()
                    .caption("Continent")
                    .groupBy(true)
                    .beanProperty("name"),
            Continent.SURFACE_AREA.define()
                    .column()
                    .caption("Surface area")
                    .expression("sum(surfacearea)")
                    .aggregate(true)
                    .numberFormatGrouping(true),
            Continent.POPULATION.define()
                    .column()
                    .caption("Population")
                    .expression("sum(population)")
                    .aggregate(true)
                    .numberFormatGrouping(true),
            Continent.MIN_LIFE_EXPECTANCY.define()
                    .column()
                    .caption("Min. life expectancy")
                    .expression("min(lifeexpectancy)")
                    .aggregate(true),
            Continent.MAX_LIFE_EXPECTANCY.define()
                    .column()
                    .caption("Max. life expectancy")
                    .expression("max(lifeexpectancy)")
                    .aggregate(true),
            Continent.MIN_INDEPENDENCE_YEAR.define()
                    .column()
                    .caption("Min. ind. year")
                    .expression("min(indepyear)")
                    .aggregate(true),
            Continent.MAX_INDEPENDENCE_YEAR.define()
                    .column()
                    .caption("Max. ind. year")
                    .expression("max(indepyear)")
                    .aggregate(true),
            Continent.GNP.define()
                    .column()
                    .caption("GNP")
                    .expression("sum(gnp)")
                    .aggregate(true)
                    .numberFormatGrouping(true))
            .tableName("world.country")
            .readOnly(true)
            .caption("Continent"));
  }

Model

ContinentModel
package is.codion.framework.demos.world.model;

import is.codion.common.model.table.ColumnConditionModel;
import is.codion.common.model.table.ColumnConditionModel.AutomaticWildcard;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingDetailModelLink;
import is.codion.swing.framework.model.SwingEntityModel;

import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;

import java.util.Collection;

public final class ContinentModel extends SwingEntityModel {

  private final DefaultPieDataset<String> surfaceAreaDataset = new DefaultPieDataset<>();
  private final DefaultPieDataset<String> populationDataset = new DefaultPieDataset<>();
  private final DefaultPieDataset<String> gnpDataset = new DefaultPieDataset<>();
  private final DefaultCategoryDataset lifeExpectancyDataset = new DefaultCategoryDataset();

  ContinentModel(EntityConnectionProvider connectionProvider) {
    super(Continent.TYPE, connectionProvider);
    tableModel().refresher().addRefreshListener(this::refreshChartDatasets);
    CountryModel countryModel = new CountryModel(connectionProvider);
    addDetailModel(new CountryModelHandler(countryModel)).active().set(true);
  }

  public PieDataset<String> populationDataset() {
    return populationDataset;
  }

  public PieDataset<String> surfaceAreaDataset() {
    return surfaceAreaDataset;
  }

  public PieDataset<String> gnpDataset() {
    return gnpDataset;
  }

  public CategoryDataset lifeExpectancyDataset() {
    return lifeExpectancyDataset;
  }

  private void refreshChartDatasets() {
    populationDataset.clear();
    surfaceAreaDataset.clear();
    gnpDataset.clear();
    lifeExpectancyDataset.clear();
    Entity.castTo(Continent.class, tableModel().items()).forEach(continent -> {
      populationDataset.setValue(continent.name(), continent.population());
      surfaceAreaDataset.setValue(continent.name(), continent.surfaceArea());
      gnpDataset.setValue(continent.name(), continent.gnp());
      lifeExpectancyDataset.addValue(continent.minLifeExpectancy(), "Lowest", continent.name());
      lifeExpectancyDataset.addValue(continent.maxLifeExpectancy(), "Highest", continent.name());
    });
  }

  private static final class CountryModel extends SwingEntityModel {

    private CountryModel(EntityConnectionProvider connectionProvider) {
      super(Country.TYPE, connectionProvider);
      editModel().readOnly().set(true);
      ColumnConditionModel<?, ?> continentConditionModel =
              tableModel().conditionModel().conditionModel(Country.CONTINENT);
      continentConditionModel.automaticWildcard().set(AutomaticWildcard.NONE);
      continentConditionModel.caseSensitive().set(true);
    }
  }

  private static final class CountryModelHandler extends SwingDetailModelLink {

    private CountryModelHandler(SwingEntityModel detailModel) {
      super(detailModel);
    }

    @Override
    public void onSelection(Collection<Entity> selectedEntities) {
      Collection<String> continentNames = Entity.values(Continent.NAME, selectedEntities);
      if (detailModel().tableModel().conditionModel().setEqualConditionValues(Country.CONTINENT, continentNames)) {
        detailModel().tableModel().refresh();
      }
    }
  }
}

UI

ContinentPanel
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.model.ContinentModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityPanel;

import org.jfree.chart.ChartPanel;

import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import java.awt.BorderLayout;
import java.awt.Dimension;

import static is.codion.framework.demos.world.ui.ChartPanels.createBarChartPanel;
import static is.codion.framework.demos.world.ui.ChartPanels.createPieChartPanel;
import static is.codion.swing.common.ui.component.Components.*;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static java.awt.event.KeyEvent.VK_1;
import static java.awt.event.KeyEvent.VK_2;

final class ContinentPanel extends EntityPanel {

  private final EntityPanel countryPanel;

  ContinentPanel(SwingEntityModel continentModel) {
    super(continentModel, new ContinentTablePanel(continentModel.tableModel()),
            new ContinentPanelLayout());
    countryPanel = new EntityPanel(continentModel.detailModel(Country.TYPE));
    countryPanel.tablePanel().configure().includeConditionPanel(false);
  }

  private static final class ContinentPanelLayout implements PanelLayout {

    @Override
    public void layout(EntityPanel entityPanel) {
      ContinentPanel continentPanel = (ContinentPanel) entityPanel;
      ContinentModel model = entityPanel.model();

      ChartPanel populationChartPanel = createPieChartPanel(entityPanel, model.populationDataset(), "Population");
      ChartPanel surfaceAreaChartPanel = createPieChartPanel(entityPanel, model.surfaceAreaDataset(), "Surface area");
      ChartPanel gnpChartPanel = createPieChartPanel(entityPanel, model.gnpDataset(), "GNP");
      ChartPanel lifeExpectancyChartPanel = createBarChartPanel(entityPanel, model.lifeExpectancyDataset(), "Life expectancy", "Continent", "Years");
      lifeExpectancyChartPanel.setPreferredSize(new Dimension(lifeExpectancyChartPanel.getPreferredSize().width, 120));

      Dimension pieChartSize = new Dimension(260, 260);
      populationChartPanel.setPreferredSize(pieChartSize);
      surfaceAreaChartPanel.setPreferredSize(pieChartSize);
      gnpChartPanel.setPreferredSize(pieChartSize);

      JPanel pieChartChartPanel = gridLayoutPanel(1, 3)
              .add(populationChartPanel)
              .add(surfaceAreaChartPanel)
              .add(gnpChartPanel)
              .build();

      JPanel chartPanel = borderLayoutPanel()
              .northComponent(lifeExpectancyChartPanel)
              .centerComponent(pieChartChartPanel)
              .build();

      continentPanel.countryPanel.initialize();
      continentPanel.countryPanel.setPreferredSize(new Dimension(continentPanel.countryPanel.getPreferredSize().width, 100));

      JTabbedPane tabbedPane = tabbedPane()
              .tabBuilder("Charts", chartPanel)
              .mnemonic(VK_1)
              .add()
              .tabBuilder("Countries", continentPanel.countryPanel)
              .mnemonic(VK_2)
              .add()
              .build();

      continentPanel.setLayout(borderLayout());

      continentPanel.add(continentPanel.editControlTablePanel(), BorderLayout.CENTER);
      continentPanel.add(tabbedPane, BorderLayout.SOUTH);

      continentPanel.setupKeyboardActions();
      continentPanel.setupNavigation();
    }
  }
}
ContinentTablePanel
package is.codion.framework.demos.world.ui;

import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;

import javax.swing.JTable;
import java.util.List;

final class ContinentTablePanel extends EntityTablePanel {

  ContinentTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
    configure().includeSouthPanel(false);
    table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
  }

  @Override
  protected Controls createPopupMenuControls(List<Controls> additionalPopupMenuControls) {
    return Controls.controls(control(TableControl.REFRESH).get());
  }
}

Lookup

Lookup

Domain

API
  interface Lookup {
    EntityType TYPE = DOMAIN.entityType("world.country_city_lookup");

    Column<String> COUNTRY_CODE = TYPE.stringColumn("country.code");
    Column<String> COUNTRY_NAME = TYPE.stringColumn("country.name");
    Column<String> COUNTRY_CONTINENT = TYPE.stringColumn("country.continent");
    Column<String> COUNTRY_REGION = TYPE.stringColumn("country.region");
    Column<Double> COUNTRY_SURFACEAREA = TYPE.doubleColumn("country.surfacearea");
    Column<Integer> COUNTRY_INDEPYEAR = TYPE.integerColumn("country.indepyear");
    Column<Integer> COUNTRY_POPULATION = TYPE.integerColumn("country.population");
    Column<Double> COUNTRY_LIFEEXPECTANCY = TYPE.doubleColumn("country.lifeexpectancy");
    Column<Double> COUNTRY_GNP = TYPE.doubleColumn("country.gnp");
    Column<Double> COUNTRY_GNPOLD = TYPE.doubleColumn("country.gnpold");
    Column<String> COUNTRY_LOCALNAME = TYPE.stringColumn("country.localname");
    Column<String> COUNTRY_GOVERNMENTFORM = TYPE.stringColumn("country.governmentform");
    Column<String> COUNTRY_HEADOFSTATE = TYPE.stringColumn("country.headofstate");
    Column<String> COUNTRY_CODE2 = TYPE.stringColumn("country.code2");
    Column<byte[]> COUNTRY_FLAG = TYPE.byteArrayColumn("country.flag");
    Column<Integer> CITY_ID = TYPE.integerColumn("city.id");
    Column<String> CITY_NAME = TYPE.stringColumn("city.name");
    Column<String> CITY_DISTRICT = TYPE.stringColumn("city.district");
    Column<Integer> CITY_POPULATION = TYPE.integerColumn("city.population");
    Column<Location> CITY_LOCATION = TYPE.column("city.location", Location.class);
  }
Implementation
  void lookup() {
    add(Lookup.TYPE.define(
            Lookup.COUNTRY_CODE.define()
                    .primaryKey(0)
                    .caption("Country code"),
            Lookup.COUNTRY_NAME.define()
                    .column()
                    .caption("Country name"),
            Lookup.COUNTRY_CONTINENT.define()
                    .column()
                    .caption("Continent")
                    .items(CONTINENT_ITEMS),
            Lookup.COUNTRY_REGION.define()
                    .column()
                    .caption("Region"),
            Lookup.COUNTRY_SURFACEAREA.define()
                    .column()
                    .caption("Surface area")
                    .numberFormatGrouping(true),
            Lookup.COUNTRY_INDEPYEAR.define()
                    .column()
                    .caption("Indep. year"),
            Lookup.COUNTRY_POPULATION.define()
                    .column()
                    .caption("Country population")
                    .numberFormatGrouping(true),
            Lookup.COUNTRY_LIFEEXPECTANCY.define()
                    .column()
                    .caption("Life expectancy"),
            Lookup.COUNTRY_GNP.define()
                    .column()
                    .caption("GNP")
                    .numberFormatGrouping(true),
            Lookup.COUNTRY_GNPOLD.define()
                    .column()
                    .caption("GNP old")
                    .numberFormatGrouping(true),
            Lookup.COUNTRY_LOCALNAME.define()
                    .column()
                    .caption("Local name"),
            Lookup.COUNTRY_GOVERNMENTFORM.define()
                    .column()
                    .caption("Government form"),
            Lookup.COUNTRY_HEADOFSTATE.define()
                    .column()
                    .caption("Head of state"),
            Lookup.COUNTRY_FLAG.define()
                    .column()
                    .caption("Flag")
                    .lazy(true),
            Lookup.COUNTRY_CODE2.define()
                    .column()
                    .caption("Code2"),
            Lookup.CITY_ID.define()
                    .primaryKey(1),
            Lookup.CITY_NAME.define()
                    .column()
                    .caption("City"),
            Lookup.CITY_DISTRICT.define()
                    .column()
                    .caption("District"),
            Lookup.CITY_POPULATION.define()
                    .column()
                    .caption("City population")
                    .numberFormatGrouping(true),
            Lookup.CITY_LOCATION.define()
                    .column()
                    .caption("City location")
                    .columnClass(String.class, new LocationConverter())
                    .comparator(new LocationComparator()))
            .selectQuery(SelectQuery.builder()
                    .from("world.country left outer join world.city on city.countrycode = country.code")
                    .build())
            .orderBy(OrderBy.builder()
                    .ascending(Lookup.COUNTRY_NAME)
                    .descending(Lookup.CITY_POPULATION)
                    .build())
            .readOnly(true)
            .caption("Lookup"));
  }

Model

LookupTableModel
package is.codion.framework.demos.world.model;

import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.json.domain.EntityObjectMapper;
import is.codion.swing.framework.model.SwingEntityTableModel;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;

import static is.codion.common.Text.textFileContents;
import static is.codion.framework.json.domain.EntityObjectMapper.entityObjectMapper;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

public final class LookupTableModel extends SwingEntityTableModel {

  public enum ExportFormat {
    CSV {
      @Override
      public String defaultFileName() {
        return "export.csv";
      }
    },
    JSON {
      @Override
      public String defaultFileName() {
        return "export.json";
      }
    };

    public abstract String defaultFileName();
  }

  private final EntityObjectMapper objectMapper = entityObjectMapper(entities());

  LookupTableModel(EntityConnectionProvider connectionProvider) {
    super(Lookup.TYPE, connectionProvider);
    objectMapper.setIncludeNullValues(false);
  }

  public void export(File file, ExportFormat format) throws IOException {
    requireNonNull(file);
    requireNonNull(format);
    switch (format) {
      case CSV -> exportCSV(file);
      case JSON -> exportJSON(file);
      default -> throw new IllegalArgumentException("Unknown export format: " + format);
    }
  }

  public void importJSON(File file) throws IOException {
    List<Entity> entities = objectMapper.deserializeEntities(textFileContents(file, UTF_8));
    clear();
    conditionModel().clear();
    addItemsAtSorted(0, entities);
  }

  private void exportCSV(File file) throws IOException {
    Files.writeString(file.toPath(), rowsAsDelimitedString(','));
  }

  private void exportJSON(File file) throws IOException {
    Collection<Entity> entities = selectionModel().isSelectionEmpty() ? items() : selectionModel().getSelectedItems();
    Files.writeString(file.toPath(), objectMapper.serializeEntities(entities));
  }
}

UI

LookupTablePanel
package is.codion.framework.demos.world.ui;

import is.codion.common.state.State;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.demos.world.model.LookupTableModel;
import is.codion.framework.demos.world.model.LookupTableModel.ExportFormat;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.ui.Utilities;
import is.codion.swing.common.ui.component.button.ToggleButtonType;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.common.ui.control.ToggleControl;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import is.codion.swing.framework.ui.icon.FrameworkIcons;

import org.jxmapviewer.JXMapKit;
import org.kordamp.ikonli.foundation.Foundation;

import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;

import static is.codion.framework.demos.world.model.LookupTableModel.ExportFormat.CSV;
import static is.codion.framework.demos.world.model.LookupTableModel.ExportFormat.JSON;
import static is.codion.swing.common.ui.component.Components.scrollPane;
import static is.codion.swing.common.ui.component.Components.toolBar;
import static java.util.stream.Collectors.toSet;
import static javax.swing.BorderFactory.createTitledBorder;

final class LookupTablePanel extends EntityTablePanel {

  private static final Dimension DEFAULT_MAP_SIZE = new Dimension(400, 400);

  private final State columnSelectionPanelVisible = State.state(true);
  private final State mapDialogVisible = State.state();

  private final Control toggleMapControl = ToggleControl.builder(mapDialogVisible)
          .smallIcon(FrameworkIcons.instance().icon(Foundation.MAP))
          .name("Show map")
          .build();
  private final JScrollPane columnSelectionScrollPane = scrollPane(createColumnSelectionToolBar())
          .verticalUnitIncrement(16)
          .build();
  private final JXMapKit mapKit = Maps.createMapKit();

  private JDialog mapKitDialog;

  LookupTablePanel(SwingEntityTableModel lookupModel) {
    super(lookupModel);
    columnSelectionPanelVisible.addDataListener(this::setColumnSelectionPanelVisible);
    table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    conditionPanelVisible().set(true);
    showRefreshProgressBar().set(true);
    bindEvents();
  }

  @Override
  public void updateUI() {
    super.updateUI();
    Utilities.updateComponentTreeUI(mapKit);
  }

  @Override
  protected void setupControls() {
    control(TableControl.CLEAR).set(Control.builder(this::clearTableAndConditions)
            .name("Clear")
            .mnemonic('C')
            .smallIcon(FrameworkIcons.instance().clear())
            .build());
    control(TableControl.SELECT_COLUMNS).set(ToggleControl.builder(columnSelectionPanelVisible)
            .name("Select")
            .build());
  }

  @Override
  protected Controls createPopupMenuControls(List<Controls> additionalPopupMenuControls) {
    FrameworkIcons icons = FrameworkIcons.instance();

    return super.createPopupMenuControls(additionalPopupMenuControls)
            .addSeparatorAt(2)
            .addAt(3, Controls.builder()
                    .name("Export")
                    .smallIcon(icons.icon(Foundation.PAGE_EXPORT))
                    .control(Control.builder(this::exportCSV)
                            .name("CSV..."))
                    .control(Control.builder(this::exportJSON)
                            .name("JSON..."))
                    .build())
            .addAt(4, Controls.builder()
                    .name("Import")
                    .smallIcon(icons.icon(Foundation.PAGE_ADD))
                    .control(Control.builder(this::importJSON)
                            .name("JSON..."))
                    .build())
            .addSeparatorAt(5)
            .addAt(6, toggleMapControl);
  }

  @Override
  protected Controls createToolBarControls(List<Controls> additionalToolBarControls) {
    return super.createToolBarControls(additionalToolBarControls)
            .addAt(0, toggleMapControl);
  }

  @Override
  protected void layoutPanel(JComponent tableComponent, JPanel southPanel) {
    super.layoutPanel(tableComponent, southPanel);
    add(columnSelectionScrollPane, BorderLayout.EAST);
  }

  private void bindEvents() {
    mapDialogVisible.addDataListener(this::setMapDialogVisible);
    tableModel().addDataChangedListener(this::displayCityLocations);
    tableModel().selectionModel().addSelectionListener(this::displayCityLocations);
  }

  private void displayCityLocations() {
    if (mapKit.isShowing()) {
      Collection<Entity> entities = tableModel().selectionModel().isSelectionEmpty() ?
              tableModel().visibleItems() :
              tableModel().selectionModel().getSelectedItems();
      Maps.paintWaypoints(entities.stream()
              .filter(entity -> entity.isNotNull(Lookup.CITY_LOCATION))
              .map(entity -> entity.get(Lookup.CITY_LOCATION))
              .collect(toSet()), mapKit.getMainMap());
    }
  }

  private void setMapDialogVisible(boolean visible) {
    if (mapKitDialog == null) {
      mapKitDialog = Dialogs.componentDialog(mapKit)
              .owner(this)
              .modal(false)
              .title("World Map")
              .size(DEFAULT_MAP_SIZE)
              .onShown(dialog -> displayCityLocations())
              .onClosed(e -> mapDialogVisible.set(false))
              .build();
    }
    mapKitDialog.setVisible(visible);
  }

  private void exportCSV() {
    export(CSV);
  }

  private void exportJSON() {
    export(JSON);
  }

  private void export(ExportFormat format) {
    File fileToSave = Dialogs.fileSelectionDialog()
            .owner(this)
            .selectFileToSave(format.defaultFileName());
    LookupTableModel lookupTableModel = tableModel();
    Dialogs.progressWorkerDialog(() -> lookupTableModel.export(fileToSave, format))
            .owner(this)
            .title("Exporting data")
            .onResult("Export successful")
            .onException("Export failed")
            .execute();
  }

  private void importJSON() throws IOException {
    File file = Dialogs.fileSelectionDialog()
            .owner(this)
            .fileFilter(new FileNameExtensionFilter("JSON", "json"))
            .selectFile();
    LookupTableModel tableModel = tableModel();
    tableModel.importJSON(file);
  }

  private JToolBar createColumnSelectionToolBar() {
    Controls toggleColumnsControls = table().createToggleColumnsControls();

    return toolBar(Controls.controls()
            .add(createSelectAllColumnsControl(toggleColumnsControls))
            .addSeparator()
            .addAll(toggleColumnsControls))
            .floatable(false)
            .orientation(SwingConstants.VERTICAL)
            .toggleButtonType(ToggleButtonType.CHECKBOX)
            .includeButtonText(true)
            .border(createTitledBorder("Columns"))
            .build();
  }

  private void setColumnSelectionPanelVisible(boolean visible) {
    columnSelectionScrollPane.setVisible(visible);
    revalidate();
  }

  private void clearTableAndConditions() {
    tableModel().clear();
    tableModel().conditionModel().clear();
  }

  private static Control createSelectAllColumnsControl(Controls toggleColumnsControls) {
    return Control.builder(() -> toggleColumnsControls.actions().stream()
                    .map(ToggleControl.class::cast)
                    .forEach(toggleControl -> toggleControl.value().set(true)))
            .name("Select all")
            .smallIcon(FrameworkIcons.instance().icon(Foundation.CHECK))
            .build();
  }
}

Common classes

UI

package is.codion.framework.demos.world.ui;

import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;

import org.jfree.chart.ChartPanel;
import org.jfree.data.general.PieDataset;

import javax.swing.JComponent;
import javax.swing.JPanel;

import static is.codion.framework.demos.world.ui.ChartPanels.createPieChartPanel;
import static is.codion.swing.common.ui.component.Components.borderLayoutPanel;
import static is.codion.swing.common.ui.component.Components.tabbedPane;
import static java.awt.event.KeyEvent.VK_1;
import static java.awt.event.KeyEvent.VK_2;

abstract class ChartTablePanel extends EntityTablePanel {

  private final ChartPanel chartPanel;

  protected ChartTablePanel(SwingEntityTableModel tableModel, PieDataset<String> chartDataset,
                            String chartTitle) {
    super(tableModel);
    chartPanel = createPieChartPanel(this, chartDataset, chartTitle);
  }

  @Override
  protected final void layoutPanel(JComponent tableComponent, JPanel southPanel) {
    super.layoutPanel(tabbedPane()
            .tabBuilder("Table", borderLayoutPanel()
                    .centerComponent(tableComponent)
                    .southComponent(southPanel)
                    .build())
            .mnemonic(VK_1)
            .add()
            .tabBuilder("Chart", chartPanel)
            .mnemonic(VK_2)
            .add()
            .build(), southPanel);
  }
}
package is.codion.framework.demos.world.ui;

import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.general.PieDataset;

import javax.swing.JComponent;
import javax.swing.UIManager;
import java.awt.Color;

import static org.jfree.chart.ChartFactory.createBarChart;
import static org.jfree.chart.ChartFactory.createPieChart;

final class ChartPanels {

  private ChartPanels() {}

  static ChartPanel createPieChartPanel(JComponent parent, PieDataset<String> dataset, String title) {
    JFreeChart chart = createPieChart(title, dataset);
    chart.removeLegend();
    linkColors(parent, chart);

    return new ChartPanel(chart);
  }

  static ChartPanel createBarChartPanel(JComponent parent, CategoryDataset dataset, String title,
                                        String categoryLabel, String valueLabel) {
    JFreeChart chart = createBarChart(title, categoryLabel, valueLabel, dataset);
    linkColors(parent, chart);

    return new ChartPanel(chart);
  }

  private static void linkColors(JComponent parent, JFreeChart chart) {
    setColors(chart, parent.getBackground());
    parent.addPropertyChangeListener("background", evt ->
            setColors(chart, (Color) evt.getNewValue()));
  }

  private static void setColors(JFreeChart chart, Color backgroundColor) {
    chart.setBackgroundPaint(backgroundColor);
    Plot plot = chart.getPlot();
    plot.setBackgroundPaint(backgroundColor);
    Color textFieldForeground = UIManager.getColor("TextField.foreground");
    if (plot instanceof PiePlot<?> piePlot) {
      piePlot.setLabelBackgroundPaint(textFieldForeground);
      piePlot.setLabelPaint(backgroundColor);
    }
    if (plot instanceof CategoryPlot categoryPlot) {
      categoryPlot.getDomainAxis().setLabelPaint(textFieldForeground);
      categoryPlot.getRangeAxis().setLabelPaint(textFieldForeground);
      categoryPlot.getDomainAxis().setTickLabelPaint(textFieldForeground);
      categoryPlot.getRangeAxis().setTickLabelPaint(textFieldForeground);
    }
    LegendTitle legend = chart.getLegend();
    if (legend != null) {
      legend.setBackgroundPaint(backgroundColor);
      legend.setItemPaint(textFieldForeground);
    }
    chart.getTitle().setPaint(textFieldForeground);
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.domain.api.World.Location;

import org.jxmapviewer.JXMapKit;
import org.jxmapviewer.JXMapViewer;
import org.jxmapviewer.OSMTileFactoryInfo;
import org.jxmapviewer.viewer.DefaultTileFactory;
import org.jxmapviewer.viewer.DefaultWaypoint;
import org.jxmapviewer.viewer.GeoPosition;
import org.jxmapviewer.viewer.Waypoint;
import org.jxmapviewer.viewer.WaypointPainter;

import javax.swing.SwingUtilities;
import java.util.Set;

import static java.util.stream.Collectors.toSet;

final class Maps {

  private static final int MIN_ZOOM = 19;
  private static final int SINGLE_WAYPOINT_ZOOM_LEVEL = 15;

  private Maps() {}

  static JXMapKit createMapKit() {
    JXMapKit mapKit = new JXMapKit();
    mapKit.setTileFactory(new DefaultTileFactory(new OSMTileFactoryInfo()));
    mapKit.setMiniMapVisible(false);
    mapKit.setZoomSliderVisible(false);
    mapKit.setZoomButtonsVisible(false);
    mapKit.getMainMap().setZoom(MIN_ZOOM);
    mapKit.getMainMap().setOverlayPainter(new WaypointPainter<>());

    return mapKit;
  }

  static void paintWaypoints(Set<Location> positions, JXMapViewer mapViewer) {
    Set<GeoPosition> geoPositions = positions.stream()
            .map(location -> new GeoPosition(location.latitude(), location.longitude()))
            .collect(toSet());
    WaypointPainter<Waypoint> overlayPainter = (WaypointPainter<Waypoint>) mapViewer.getOverlayPainter();
    overlayPainter.setWaypoints(geoPositions.stream()
            .map(DefaultWaypoint::new)
            .collect(toSet()));
    SwingUtilities.invokeLater(() -> {
      if (geoPositions.isEmpty()) {
        mapViewer.setZoom(MIN_ZOOM);
        mapViewer.repaint();
      }
      else if (geoPositions.size() == 1) {
        mapViewer.setZoom(0);
        mapViewer.setCenterPosition(geoPositions.iterator().next());
        mapViewer.setZoom(SINGLE_WAYPOINT_ZOOM_LEVEL);
      }
      else {
        mapViewer.setZoom(0);
        mapViewer.zoomToBestFit(geoPositions, 1);
      }
    });
  }
}

Application

WorldAppModel

package is.codion.framework.demos.world.model;

import is.codion.common.version.Version;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityApplicationModel;
import is.codion.swing.framework.model.SwingEntityModel;

public final class WorldAppModel extends SwingEntityApplicationModel {

  public static final Version VERSION = Version.parsePropertiesFile(WorldAppModel.class, "/version.properties");

  public WorldAppModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider, VERSION);
    CountryModel countryModel = new CountryModel(connectionProvider);
    SwingEntityModel lookupModel = new SwingEntityModel(new LookupTableModel(connectionProvider));
    ContinentModel continentModel = new ContinentModel(connectionProvider);

    countryModel.tableModel().refresh();
    continentModel.tableModel().refresh();

    addEntityModels(countryModel, lookupModel, continentModel);
  }
}

UI

WorldAppPanel

package is.codion.framework.demos.world.ui;

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.framework.demos.world.domain.api.World;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.demos.world.model.CountryModel;
import is.codion.framework.demos.world.model.WorldAppModel;
import is.codion.swing.common.ui.component.table.FilteredTableCellRenderer;
import is.codion.swing.common.ui.laf.LookAndFeelProvider;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.ReferentialIntegrityErrorHandling;
import is.codion.swing.framework.ui.icon.FrameworkIcons;

import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes;
import org.kordamp.ikonli.foundation.Foundation;

import javax.swing.SwingConstants;
import java.awt.Dimension;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public final class WorldAppPanel extends EntityApplicationPanel<WorldAppModel> {

  private static final String DEFAULT_FLAT_LOOK_AND_FEEL = "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme";

  public WorldAppPanel(WorldAppModel appModel) {
    super(appModel);
    FrameworkIcons.instance().add(Foundation.MAP, Foundation.PAGE_EXPORT, Foundation.PAGE_ADD, Foundation.CHECK);
  }

  @Override
  protected List<EntityPanel> createEntityPanels() {
    CountryModel countryModel = applicationModel().entityModel(Country.TYPE);
    CountryPanel countryPanel = new CountryPanel(countryModel);

    SwingEntityModel continentModel = applicationModel().entityModel(Continent.TYPE);
    ContinentPanel continentPanel = new ContinentPanel(continentModel);

    SwingEntityModel lookupModel = applicationModel().entityModel(Lookup.TYPE);
    EntityPanel lookupPanel = new EntityPanel(lookupModel,
            new LookupTablePanel(lookupModel.tableModel()));

    return List.of(countryPanel, continentPanel, lookupPanel);
  }

  public static void main(String[] args) throws CancelException {
    Locale.setDefault(new Locale("en", "EN"));
    Arrays.stream(FlatAllIJThemes.INFOS).forEach(LookAndFeelProvider::addLookAndFeelProvider);
    EntityPanel.TOOLBAR_CONTROLS.set(true);
    FilteredTableCellRenderer.NUMERICAL_HORIZONTAL_ALIGNMENT.set(SwingConstants.CENTER);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING.set(ReferentialIntegrityErrorHandling.DISPLAY_DEPENDENCIES);
    EntityApplicationPanel.builder(WorldAppModel.class, WorldAppPanel.class)
            .applicationName("World")
            .domainType(World.DOMAIN)
            .applicationVersion(WorldAppModel.VERSION)
            .defaultLookAndFeelClassName(DEFAULT_FLAT_LOOK_AND_FEEL)
            .frameSize(new Dimension(1280, 720))
            .defaultLoginUser(User.parse("scott:tiger"))
            .start();
  }
}

Domain unit test

package is.codion.framework.demos.world.domain;

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

import java.util.Map;

public final class WorldImplTest extends EntityTestUnit {

  public WorldImplTest() {
    super(new WorldImpl());
  }

  @Test
  void country() throws DatabaseException {
    test(Country.TYPE);
  }

  @Test
  void city() throws DatabaseException {
    test(City.TYPE);
  }

  @Test
  void countryLanguage() throws DatabaseException {
    test(CountryLanguage.TYPE);
  }

  @Test
  void lookup() throws DatabaseException {
    connection().selectSingle(Lookup.CITY_NAME.equalTo("Genova"));
  }

  @Override
  protected Entity initializeTestEntity(EntityType entityType,
                                        Map<ForeignKey, Entity> foreignKeyEntities) {
    Entity entity = super.initializeTestEntity(entityType, foreignKeyEntities);
    if (entityType.equals(Country.TYPE)) {
      entity.put(Country.CODE, "XYZ");
      entity.put(Country.CONTINENT, "Asia");
    }
    else if (entityType.equals(City.TYPE)) {
      entity.remove(City.LOCATION);
    }

    return entity;
  }

  @Override
  protected void modifyEntity(Entity testEntity, Map<ForeignKey, Entity> foreignKeyEntities) {
    super.modifyEntity(testEntity, foreignKeyEntities);
    if (testEntity.entityType().equals(Country.TYPE)) {
      testEntity.put(Country.CONTINENT, "Europe");
    }
    else if (testEntity.entityType().equals(City.TYPE)) {
      testEntity.put(City.LOCATION, null);
    }
  }

  @Override
  protected Entity initializeForeignKeyEntity(ForeignKey foreignKey,
                                              Map<ForeignKey, Entity> foreignKeyEntities)
          throws DatabaseException {
    if (foreignKey.referencedType().equals(Country.TYPE)) {
      return entities().builder(Country.TYPE)
              .with(Country.CODE, "ISL")
              .build();
    }
    if (foreignKey.referencedType().equals(City.TYPE)) {
      return entities().builder(City.TYPE)
              .with(City.ID, 1449)
              .build();
    }

    return super.initializeForeignKeyEntity(foreignKey, foreignKeyEntities);
  }
}

Module Info

Domain

API

/**
 * Domain API.
 */
module is.codion.framework.demos.world.domain.api {
  requires transitive is.codion.framework.domain;
  requires transitive is.codion.framework.db.core;

  exports is.codion.framework.demos.world.domain.api;

  //for accessing default methods in EntityType interfaces
  opens is.codion.framework.demos.world.domain.api;
}

Implementation

/**
 * Domain implementation.
 */
module is.codion.framework.demos.world.domain {
  requires transitive is.codion.framework.demos.world.domain.api;

  exports is.codion.framework.demos.world.domain;

  provides is.codion.framework.domain.Domain
          with is.codion.framework.demos.world.domain.WorldImpl;
}

Client

/**
 * Client.
 */
module is.codion.framework.demos.world.client {
  requires is.codion.framework.json.domain;
  requires is.codion.swing.framework.ui;
  requires is.codion.plugin.jasperreports;
  requires is.codion.framework.demos.world.domain.api;
  requires com.formdev.flatlaf.intellijthemes;
  requires org.kordamp.ikonli.foundation;
  requires org.jfree.jfreechart;
  requires jasperreports;
  requires org.jxmapviewer.jxmapviewer2;
  requires org.json;

  exports is.codion.framework.demos.world.ui
          to is.codion.swing.framework.ui;

  //for loading reports from classpath
  opens is.codion.framework.demos.world.model;
}