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 DomainModel {

  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(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 foreign key validation.
    validateForeignKeys(false);

    add(city(), country(), countryLanguage(), lookup(), continent());
    add(Country.AVERAGE_CITY_POPULATION, new AverageCityPopulationFunction());
  }

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 {
    EntityType TYPE = DOMAIN.entityType("world.country");

    Column<String> CODE = TYPE.stringColumn("code");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<String> CONTINENT = TYPE.stringColumn("continent");
    Column<String> REGION = TYPE.stringColumn("region");
    Column<Double> SURFACEAREA = TYPE.doubleColumn("surfacearea");
    Column<Integer> INDEPYEAR = TYPE.integerColumn("indepyear");
    Column<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");
  }
Implementation
  EntityDefinition country() {
    return 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")
            .build();
  }
  private static final class AverageCityPopulationFunction implements DatabaseFunction<EntityConnection, String, Double> {

    @Override
    public Double execute(EntityConnection connection, String countryCode) {
      return connection.select(where(City.COUNTRY_CODE.equalTo(countryCode))
                      .attributes(City.POPULATION)
                      .build()).stream()
              .map(city -> city.get(City.POPULATION))
              .mapToInt(Integer::intValue)
              .average()
              .orElse(0d);
    }
  }

Model

CountryModel
package is.codion.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.demos.world.model;

import is.codion.common.value.Value;
import is.codion.common.value.ValueObserver;
import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
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;

import java.util.Objects;

public final class CountryEditModel extends SwingEntityEditModel {

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

  CountryEditModel(EntityConnectionProvider connectionProvider) {
    super(Country.TYPE, connectionProvider);
    initializeComboBoxModels(Country.CAPITAL_FK);
    entity().addConsumer(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
      entity().addConsumer(country ->
              comboBoxModel.filter().predicate().set(city ->
                      country != null && Objects.equals(city.get(City.COUNTRY_FK), country)));
    }

    return comboBoxModel;
  }

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

  private Double averageCityPopulation(Entity country) {
    if (country == null) {
      return null;
    }
    return connection().execute(Country.AVERAGE_CITY_POPULATION, country.get(Country.CODE));
  }
}
CountryTableModel
package is.codion.demos.world.model;

import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.condition.Condition;
import is.codion.framework.model.ForeignKeyConditionModel;
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) {
    CountryReportDataSource dataSource =
            new CountryReportDataSource(selection().items().get().iterator(),
                    connection(), progressReporter);

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

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

  private void configureCapitalConditionModel() {
    ForeignKeyConditionModel capitalCondition =
            (ForeignKeyConditionModel) queryModel().conditions()
                    .attribute(Country.CAPITAL_FK);
    CapitalConditionSupplier cityIsCapital = new CapitalConditionSupplier();
    capitalCondition.equalSearchModel().condition().set(cityIsCapital);
    capitalCondition.inSearchModel().condition().set(cityIsCapital);
  }

  private final class CapitalConditionSupplier implements Supplier<Condition> {
    @Override
    public Condition get() {
      return City.ID.in(connection().select(Country.CAPITAL));
    }
  }
}
package is.codion.demos.world.model;

import is.codion.common.user.User;
import is.codion.demos.world.domain.WorldImpl;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperPrint;
import org.junit.jupiter.api.Test;

public final class CountryTableModelTest {

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

  @Test
  void fillCountryReport() throws JRException {
    try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
      CountryTableModel tableModel = new CountryTableModel(connectionProvider);
      tableModel.queryModel().conditions().get(Country.CODE).operands().equal().set("ISL");
      tableModel.refresh();
      tableModel.selection().index().set(0);
      JasperPrint jasperPrint = tableModel.fillCountryReport(new ProgressReporter<String>() {
        @Override
        public void report(int progress) {}
        @Override
        public void publish(String... chunks) {}
      });
      JasperExportManager.getInstance(DefaultJasperReportsContext.getInstance()).exportToPdf(jasperPrint);
    }
  }

  private static EntityConnectionProvider createConnectionProvider() {
    return LocalEntityConnectionProvider.builder()
            .domain(new WorldImpl())
            .user(UNIT_TEST_USER)
            .build();
  }
}
country_report.jrxml
<jasperReport name="country_report" language="java" 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>is.codion.demos.world.model.CountryReportDataSource</import>
  <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 splitType="Stretch"/>
  <detail>
    <band height="52">
      <element kind="textField" uuid="52591026-2ffa-4ca3-b73d-31bb53463220" x="0" y="14" width="100" height="14">
        <expression><![CDATA[$F{name}]]></expression>
      </element>
      <element kind="textField" uuid="a37319e9-d3d7-4adc-a058-4783281221be" x="100" y="14" width="100" height="14">
        <expression><![CDATA[$F{continent}]]></expression>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <element kind="textField" uuid="94bb3563-d458-47f3-bb98-bb9914e458f7" x="200" y="14" width="140" height="14">
        <expression><![CDATA[$F{region}]]></expression>
      </element>
      <element kind="textField" uuid="7ae9640c-d57f-4cd9-b08c-b2e582ae4084" x="350" y="14" width="84" height="14" pattern="###,###,###,###" hTextAlign="Center">
        <expression><![CDATA[$F{surfacearea}]]></expression>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <element kind="textField" uuid="0aeaac02-df9f-4c84-9533-3daee90d0082" x="435" y="14" width="100" height="14" pattern="###,###,###,###" hTextAlign="Center">
        <expression><![CDATA[$F{population}]]></expression>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <element kind="subreport" uuid="fe0b73e2-e164-4dd6-a08d-9afb8d964fc2" x="-30" y="30" width="595" height="16">
        <dataSourceExpression><![CDATA[((CountryReportDataSource) $P{REPORT_DATA_SOURCE}).cityDataSource()]]></dataSourceExpression>
        <expression><![CDATA[$P{CITY_SUBREPORT}]]></expression>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <element kind="staticText" uuid="c61d049d-040d-4aba-9cf4-4aa277459142" x="0" y="0" width="100" height="14" bold="true">
        <text><![CDATA[Name]]></text>
      </element>
      <element kind="staticText" uuid="2fcd930a-a99f-4f90-b303-4b33938c1a34" x="100" y="0" width="100" height="14" bold="true">
        <text><![CDATA[Continent]]></text>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <element kind="staticText" uuid="510dab14-feac-4935-b8b6-130686c249dc" x="200" y="0" width="140" height="14" bold="true">
        <text><![CDATA[Region]]></text>
      </element>
      <element kind="staticText" uuid="2ee9691c-9b08-44ea-a57f-fc3e58608a95" x="350" y="0" width="84" height="14" bold="true" hTextAlign="Center">
        <text><![CDATA[Surface area]]></text>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <element kind="staticText" uuid="ac3794a2-c395-49ea-848f-066599941669" x="435" y="0" width="100" height="14" bold="true" hTextAlign="Center">
        <text><![CDATA[Population]]></text>
        <property name="com.jaspersoft.studio.unit.height" value="px"/>
      </element>
      <property name="com.jaspersoft.studio.unit.height" value="px"/>
    </band>
  </detail>
</jasperReport>
city_report.jrxml
<jasperReport name="city_report" language="java" 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 splitType="Stretch"/>
  <columnHeader height="16">
    <element kind="staticText" uuid="ef712d38-e1c6-4922-8e38-4bd263230db5" x="350" y="2" width="84" height="14" bold="true">
      <text><![CDATA[Name]]></text>
    </element>
    <element kind="staticText" uuid="8c05be12-bf23-4fac-ac18-47a1e18f5959" x="434" y="2" width="100" height="14" bold="true" hTextAlign="Center">
      <text><![CDATA[Population]]></text>
    </element>
    <element kind="staticText" uuid="44915dc4-f2b8-4661-9b9a-8cb6e6f91f58" x="250" y="2" width="100" height="14" bold="true">
      <text><![CDATA[Largest Cities]]></text>
    </element>
    <element kind="line" uuid="468587c6-550e-4dc1-80c2-72074816c6ff" x="0" y="1" width="535" height="1">
      <property name="com.jaspersoft.studio.unit.height" value="px"/>
      <pen lineStyle="Dotted"/>
    </element>
    <property name="com.jaspersoft.studio.unit.height" value="px"/>
  </columnHeader>
  <detail>
    <band height="14">
      <element kind="textField" uuid="52591026-2ffa-4ca3-b73d-31bb53463220" x="350" y="0" width="84" height="14">
        <expression><![CDATA[$F{name}]]></expression>
      </element>
      <element kind="textField" uuid="376a08f3-5e56-45cd-974e-3635b8248373" x="434" y="0" width="100" height="14" pattern="###,###,###,###" hTextAlign="Center">
        <expression><![CDATA[$F{population}]]></expression>
      </element>
      <property name="com.jaspersoft.studio.unit.height" value="px"/>
    </band>
  </detail>
  <columnFooter height="1">
    <element kind="line" uuid="e0b89b1f-85c9-4dd3-a316-a4a8359db499" x="0" y="0" width="535" height="1">
      <property name="com.jaspersoft.studio.unit.height" value="px"/>
      <pen lineStyle="Dotted"/>
    </element>
    <property name="com.jaspersoft.studio.unit.height" value="px"/>
  </columnFooter>
</jasperReport>
CountryReportDataSource
package is.codion.demos.world.model;

import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnection;
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.Iterator;
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<Entity> {

  private final EntityConnection connection;

  CountryReportDataSource(Iterator<Entity> countryIterator,
                          EntityConnection connection,
                          ProgressReporter<String> progressReporter) {
    super(countryIterator, new CountryValueProvider(),
            new CountryReportProgressReporter(progressReporter));
    this.connection = connection;
  }

  /* See usage in src/main/reports/country_report.jrxml, subreport element */
  public JRDataSource cityDataSource() {
    Entity country = currentItem();
    Collection<Entity> largestCities =
            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());
  }

  private static final class CountryValueProvider implements BiFunction<Entity, 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(Entity country, JRField field) {
      return switch (field.getName()) {
        case NAME -> country.get(Country.NAME);
        case CONTINENT -> country.get(Country.CONTINENT);
        case REGION -> country.get(Country.REGION);
        case SURFACEAREA -> country.get(Country.SURFACEAREA);
        case POPULATION -> country.get(Country.POPULATION);
        default -> throw new IllegalArgumentException("Unknown field: " + field.getName());
      };
    }
  }

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

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

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

  private static final class CountryReportProgressReporter implements Consumer<Entity> {

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

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

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

import is.codion.common.user.User;
import is.codion.common.value.Value;
import is.codion.demos.world.domain.WorldImpl;
import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
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 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.iterator(), 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(JRException.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(JRException.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.demos.world.ui;

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

final class CountryPanel extends EntityPanel {

  CountryPanel(CountryModel countryModel) {
    super(countryModel,
            new CountryEditPanel(countryModel.editModel()),
            new CountryTablePanel(countryModel.tableModel()),
            config -> config.detailLayout(entityPanel -> TabbedDetailLayout.builder(entityPanel)
                    .splitPaneResizeWeight(0.7)
                    .build()));

    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.demos.world.ui;

import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.demos.world.model.CountryEditModel;
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.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)
            .comboBoxPreferredWidth(PREFERRED_COMBO_BOX_WIDTH)
            .includeAddButton(true);
    //add a field displaying the avarage city population for the selected country
    CountryEditModel editModel = editModel();
    NumberField<Double> averageCityPopulationField = doubleField()
            .link(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()));
    if (editModel().entity().exists().get()) {
      //if an existing country is selected, then we don't allow it to be changed
      capitalEditPanel.editModel().value(City.COUNTRY_FK).set(editModel().entity().get());
      //initialize the panel components, so we can configure the country component
      capitalEditPanel.initialize();
      //disable the country selection component
      //and change the initial focus property
      capitalEditPanel.disableCountryInput();
    }

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

import is.codion.demos.world.domain.api.World.Country;
import is.codion.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;

import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.PRINT;

final class CountryTablePanel extends EntityTablePanel {

  CountryTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel, config -> config
            .editable(attributes -> attributes.remove(Country.CAPITAL_FK)));
  }

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

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

  private JasperPrint fillCountryReport(ProgressReporter<String> progressReporter) {
    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 {
    EntityType TYPE = DOMAIN.entityType("world.city");

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

    ForeignKey COUNTRY_FK = TYPE.foreignKey("country", COUNTRY_CODE, Country.CODE);
  }
  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) {
      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");
      }
    }
  }
Implementation
  EntityDefinition city() {
    return City.TYPE.define(
                    City.ID.define()
                            .primaryKey(),
                    City.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .nullable(false)
                            .maximumLength(35),
                    City.COUNTRY_CODE.define()
                            .column()
                            .nullable(false),
                    City.COUNTRY_FK.define()
                            .foreignKey()
                            .caption("Country"),
                    City.DISTRICT.define()
                            .column()
                            .caption("District")
                            .nullable(false)
                            .maximumLength(20),
                    City.POPULATION.define()
                            .column()
                            .caption("Population")
                            .nullable(false)
                            .numberFormatGrouping(true),
                    City.LOCATION.define()
                            .column()
                            .caption("Location")
                            .columnClass(String.class, new LocationConverter())
                            .comparator(new LocationComparator()))
            .keyGenerator(sequence("world.city_seq"))
            .validator(new CityValidator())
            .orderBy(ascending(City.NAME))
            .stringFactory(City.NAME)
            .caption("City")
            .build();
  }
  private static final class LocationConverter implements Converter<Location, String> {

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

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

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

Model

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

import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.demos.world.domain.api.World.Location;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
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.List;
import java.util.Optional;

import static java.nio.charset.StandardCharsets.UTF_8;
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 {
    Location location = lookupLocation(entity().get())
            .orElseThrow(() -> new RuntimeException("Location not found for city: " + entity()));
    value(City.LOCATION).set(location);
    if (entity().modified().get()) {
      update();
    }
  }

  void populateLocation(Entity city) throws IOException {
    lookupLocation(city).ifPresent(location -> city.put(City.LOCATION, location));
    if (city.modified()) {
      update(List.of(city));
    }
  }

  private static Optional<Location> lookupLocation(Entity city) throws IOException {
    JSONArray jsonArray = toJSONArray(new URL(OPENSTREETMAP_ORG_SEARCH +
            URLEncoder.encode(city.get(City.NAME), UTF_8) + "," +
            URLEncoder.encode(city.get(City.COUNTRY_FK).get(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.demos.world.model;

import is.codion.common.event.Event;
import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressResultTask;
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.List;
import java.util.function.Consumer;

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));
    selection().items().addConsumer(displayLocationEvent);
    selection().indexes().addListener(this::updateCitiesWithoutLocationSelected);
    refresher().success().addConsumer(this::refreshChartDataset);
  }

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

  public PopulateLocationTask populateLocationTask() {
    return new PopulateLocationTask();
  }

  public void addDisplayLocationConsumer(Consumer<Collection<Entity>> consumer) {
    displayLocationEvent.addConsumer(consumer);
  }

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

  private void refreshChartDataset(Collection<Entity> cities) {
    chartDataset.clear();
    cities.forEach(city -> chartDataset.setValue(city.get(City.NAME), city.get(City.POPULATION)));
  }

  private void updateCitiesWithoutLocationSelected() {
    citiesWithoutLocationSelected.set(selection().items().get().stream()
            .anyMatch(city -> city.isNull(City.LOCATION)));
  }

  public final class PopulateLocationTask implements ProgressResultTask<Void, String> {

    private final State cancelled = State.state();
    private final Collection<Entity> cities;

    private PopulateLocationTask() {
      cities = selection().items().get().stream()
              .filter(city -> city.isNull(City.LOCATION))
              .toList();
    }

    public int maximumProgress() {
      return cities.size();
    }

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

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

    @Override
    public Void execute(ProgressReporter<String> progressReporter) throws IOException {
      Collection<Entity> updatedCities = new ArrayList<>();
      CityEditModel editModel = editModel();
      Iterator<Entity> citiesIterator = cities.iterator();
      while (citiesIterator.hasNext() && !cancelled.get()) {
        Entity city = citiesIterator.next();
        progressReporter.publish(city.get(City.COUNTRY_FK).get(Country.NAME) + " - " + city.get(City.NAME));
        editModel.populateLocation(city);
        updatedCities.add(city);
        progressReporter.report(updatedCities.size());
        displayLocationEvent.accept(List.of(city));
      }
      displayLocationEvent.accept(selection().items().get());

      return null;
    }
  }
}

UI

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

import is.codion.common.state.State;
import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.model.CityEditModel;
import is.codion.demos.world.model.CityTableModel;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.ui.control.Control;
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.JComponent;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
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.addDisplayLocationConsumer(this::displayLocation);
    configureControls(config -> config
            .control(Control.builder()
                    .command(this::populateLocation)
                    .enabled(State.and(active(),
                            editModel().entity().isNull(City.LOCATION),
                            editModel().entity().exists()))
                    .smallIcon(FrameworkIcons.instance().icon(Foundation.MAP))));
  }

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

  void disableCountryInput() {
    JComponent countryComponent = component(City.COUNTRY_FK).get();
    countryComponent.setEnabled(false);
    countryComponent.setFocusable(false);
    initialFocusAttribute().set(City.NAME);
  }

  private void populateLocation() throws IOException {
    CityEditModel editModel = editModel();
    editModel.populateLocation();
    displayLocation(List.of(editModel.entity().get()));
  }

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

import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.demos.world.model.CityTableModel;
import is.codion.demos.world.model.CityTableModel.PopulateLocationTask;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.ui.EntityTableCellRenderer;
import is.codion.swing.framework.ui.icon.FrameworkIcons;

import org.kordamp.ikonli.foundation.Foundation;

import java.awt.Color;
import java.util.Objects;

final class CityTablePanel extends ChartTablePanel {

  CityTablePanel(CityTableModel tableModel) {
    super(tableModel, tableModel.chartDataset(), "Cities", config -> config
            .cellRenderer(City.POPULATION, EntityTableCellRenderer.builder(City.POPULATION, tableModel)
                    .foreground((table, city, attribute, population) ->
                            population > 1_000_000 ? Color.YELLOW : null)
                    .build())
            .cellRenderer(City.NAME, EntityTableCellRenderer.builder(City.NAME, tableModel)
                    .foreground((table, city, attribute, name) ->
                            Objects.equals(city.get(City.ID), city.get(City.COUNTRY_FK).get(Country.CAPITAL)) ? Color.GREEN : null)
                    .build())
            .editable(attributes -> attributes.remove(City.LOCATION)));
    configurePopupMenu(layout -> layout.clear()
            .control(createPopulateLocationControl())
            .separator()
            .defaults());
  }

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

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

  private void populateLocation() {
    CityTableModel tableModel = tableModel();
    PopulateLocationTask task = tableModel.populateLocationTask();

    Dialogs.progressWorkerDialog(task)
            .owner(this)
            .title("Populating locations")
            .maximumProgress(task.maximumProgress())
            .stringPainted(true)
            .control(Control.builder()
                    .command(task::cancel)
                    .name("Cancel")
                    .enabled(task.cancelled().not()))
            .onException(this::displayPopulateException)
            .execute();
  }

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

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 {
    EntityType TYPE = DOMAIN.entityType("world.countrylanguage");

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

    ForeignKey COUNTRY_FK = TYPE.foreignKey("country_fk", COUNTRY_CODE, Country.CODE);
  }
  final class NoOfSpeakersProvider implements DerivedAttribute.Provider<Integer> {

    @Serial
    private static final long serialVersionUID = 1;

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

      return null;
    }
  }
Implementation
  EntityDefinition countryLanguage() {
    return 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")
            .build();
  }

Model

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

import is.codion.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.db.EntityConnectionProvider;
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;

import java.util.Collection;

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().success().addConsumer(this::refreshChartDataset);
  }

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

  private void refreshChartDataset(Collection<Entity> countryLanguages) {
    chartDataset.clear();
    countryLanguages.forEach(countryLanguage ->
            chartDataset.setValue(countryLanguage.get(CountryLanguage.LANGUAGE),
                    countryLanguage.get(CountryLanguage.NO_OF_SPEAKERS)));
  }
}

UI

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

import is.codion.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);
    editModel.value(CountryLanguage.IS_OFFICIAL).edited().addListener(this::update);
  }

  @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.demos.world.ui;

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

final class CountryLanguageTablePanel extends ChartTablePanel {

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

Continents

continents

Continent

Domain

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

    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");
  }
Implementation
  EntityDefinition continent() {
    return Continent.TYPE.define(
                    Continent.NAME.define()
                            .column()
                            .caption("Continent")
                            .groupBy(true),
                    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")
            .build();
  }

Model

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

import is.codion.common.model.condition.ConditionModel;
import is.codion.common.model.condition.ConditionModel.Wildcard;
import is.codion.demos.world.domain.api.World.Continent;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
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().success().addConsumer(this::refreshChartDatasets);
    CountryModel countryModel = new CountryModel(connectionProvider);
    addDetailModel(new CountryModelLink(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(Collection<Entity> continents) {
    populationDataset.clear();
    surfaceAreaDataset.clear();
    gnpDataset.clear();
    lifeExpectancyDataset.clear();
    continents.forEach(continent -> {
      String contientName = continent.get(Continent.NAME);
      populationDataset.setValue(contientName, continent.get(Continent.POPULATION));
      surfaceAreaDataset.setValue(contientName, continent.get(Continent.SURFACE_AREA));
      gnpDataset.setValue(contientName, continent.get(Continent.GNP));
      lifeExpectancyDataset.addValue(continent.get(Continent.MIN_LIFE_EXPECTANCY), "Lowest", contientName);
      lifeExpectancyDataset.addValue(continent.get(Continent.MAX_LIFE_EXPECTANCY), "Highest", contientName);
    });
  }

  private static final class CountryModel extends SwingEntityModel {

    private CountryModel(EntityConnectionProvider connectionProvider) {
      super(Country.TYPE, connectionProvider);
      editModel().readOnly().set(true);
      ConditionModel<?> continentCondition =
              tableModel().queryModel().conditions().get(Country.CONTINENT);
      continentCondition.wildcard().set(Wildcard.NONE);
      continentCondition.caseSensitive().set(true);
    }
  }

  private static final class CountryModelLink extends SwingDetailModelLink {

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

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

UI

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

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

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.demos.world.ui.ChartPanels.createBarChartPanel;
import static is.codion.demos.world.ui.ChartPanels.createPieChartPanel;
import static is.codion.swing.common.ui.Sizes.setPreferredHeight;
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 {

  ContinentPanel(ContinentModel continentModel) {
    super(continentModel, new ContinentTablePanel(continentModel.tableModel()));
  }

  @Override
  protected void initializeUI() {
    ContinentModel model = model();

    ChartPanel populationChartPanel = createPieChartPanel(this, model.populationDataset(), "Population");
    ChartPanel surfaceAreaChartPanel = createPieChartPanel(this, model.surfaceAreaDataset(), "Surface area");
    ChartPanel gnpChartPanel = createPieChartPanel(this, model.gnpDataset(), "GNP");
    ChartPanel lifeExpectancyChartPanel = createBarChartPanel(this, model.lifeExpectancyDataset(), "Life expectancy", "Continent", "Years");
    setPreferredHeight(lifeExpectancyChartPanel, 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();

    EntityTablePanel countryTablePanel =
            new EntityTablePanel(model.detailModel(Country.TYPE).tableModel(),
                    config -> config
                            .includeConditions(false)
                            .includeToolBar(false));
    setPreferredHeight(countryTablePanel, 300);

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

    setLayout(borderLayout());

    add(tablePanel().initialize(), BorderLayout.CENTER);
    add(tabbedPane, BorderLayout.SOUTH);

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

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

import javax.swing.JTable;

import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.REFRESH;

final class ContinentTablePanel extends EntityTablePanel {

  ContinentTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel, config -> config.includeSouthPanel(false));
    table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    configurePopupMenu(layout -> layout.clear()
            .control(REFRESH));
  }
}

Lookup

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
  EntityDefinition lookup() {
    return 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")
            .build();
  }

UI

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

import is.codion.common.state.State;
import is.codion.demos.world.domain.api.World.Lookup;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.json.domain.EntityObjectMapper;
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.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import static is.codion.demos.world.ui.LookupTablePanel.ExportFormat.CSV;
import static is.codion.demos.world.ui.LookupTablePanel.ExportFormat.JSON;
import static is.codion.framework.json.domain.EntityObjectMapper.entityObjectMapper;
import static is.codion.swing.common.ui.component.Components.scrollPane;
import static is.codion.swing.common.ui.component.Components.toolBar;
import static is.codion.swing.common.ui.component.table.ConditionPanel.ConditionView.SIMPLE;
import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.*;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;

final class LookupTablePanel extends EntityTablePanel {

  private static final Dimension DEFAULT_MAP_SIZE = new Dimension(400, 400);
  private static final FrameworkIcons ICONS = FrameworkIcons.instance();

  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(tableModel().entities());

  private final State columnSelectionPanelVisible = State.state(true);
  private final State mapDialogVisible = State.builder()
          .consumer(this::setMapDialogVisible)
          .build();

  private final Control toggleMapControl = Control.builder()
          .toggle(mapDialogVisible)
          .smallIcon(ICONS.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, config -> config
            .showRefreshProgressBar(true)
            .conditionView(SIMPLE));
    columnSelectionPanelVisible.addConsumer(this::setColumnSelectionPanelVisible);
    table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    configurePopupMenuAndToolBar();
    bindEvents();
  }

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

  @Override
  protected void setupControls() {
    control(CLEAR).set(Control.builder()
            .command(this::clearTableAndConditions)
            .name("Clear")
            .mnemonic('C')
            .smallIcon(ICONS.clear())
            .build());
  }

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

  private void configurePopupMenuAndToolBar() {
    configurePopupMenu(layout -> layout.clear()
            .control(REFRESH)
            .control(CLEAR)
            .separator()
            .control(Controls.builder()
                    .name("Export")
                    .smallIcon(ICONS.icon(Foundation.PAGE_EXPORT))
                    .control(Control.builder()
                            .command(this::exportCSV)
                            .name("CSV..."))
                    .control(Control.builder()
                            .command(this::exportJSON)
                            .name("JSON...")))
            .control(Controls.builder()
                    .name("Import")
                    .smallIcon(ICONS.icon(Foundation.PAGE_ADD))
                    .control(Control.builder()
                            .command(this::importJSON)
                            .name("JSON...")))
            .separator()
            .control(toggleMapControl)
            .separator()
            .control(Controls.builder()
                    .name("Columns")
                    .smallIcon(FrameworkIcons.instance().columns())
                    .control(Control.builder()
                            .toggle(columnSelectionPanelVisible)
                            .name("Select")
                            .build())
                    .control(control(RESET_COLUMNS).get())
                    .control(control(SELECT_AUTO_RESIZE_MODE).get()))
            .separator()
            .control(CONDITION_CONTROLS)
            .control(COPY_CONTROLS));

    configureToolBar(config -> config.clear()
            .control(toggleMapControl)
            .separator()
            .defaults());
  }

  private void bindEvents() {
    tableModel().items().visible().addListener(this::displayCityLocations);
    tableModel().selection().indexes().addListener(this::displayCityLocations);
  }

  private void displayCityLocations() {
    if (mapKit.isShowing()) {
      Collection<Entity> entities = tableModel().selection().empty().get() ?
              tableModel().items().visible().get() :
              tableModel().selection().items().get();
      Maps.paintWaypoints(entities.stream()
              .map(entity -> entity.optional(Lookup.CITY_LOCATION))
              .flatMap(Optional::stream)
              .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());
    Dialogs.progressWorkerDialog(() -> export(fileToSave, format))
            .owner(this)
            .title("Exporting data")
            .onResult("Export successful")
            .onException("Export failed")
            .execute();
  }

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

  private void exportCSV(File file) throws IOException {
    Files.write(file.toPath(), List.of(table().export()
            .delimiter(',')
            .selected(true)
            .get()));
  }

  private void exportJSON(File file) throws IOException {
    Collection<Entity> entities = tableModel().selection().empty().get() ?
            tableModel().items().get() :
            tableModel().selection().items().get();
    Files.writeString(file.toPath(), objectMapper.writeValueAsString(entities));
  }

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

  public void importJSON(File file) throws IOException {
    List<Entity> entities = objectMapper.deserializeEntities(
            String.join("\n", Files.readAllLines(file.toPath())));
    clearTableAndConditions();
    tableModel().items().visible().addItemsAt(0, entities);
    tableModel().items().visible().sort();
  }

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

    return toolBar(Controls.builder()
            .control(createSelectAllColumnsControl(toggleColumnsControls))
            .separator()
            .actions(toggleColumnsControls.actions()))
            .floatable(false)
            .orientation(SwingConstants.VERTICAL)
            .toggleButtonType(ToggleButtonType.CHECKBOX)
            .includeButtonText(true)
            .build();
  }

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

  private void clearTableAndConditions() {
    tableModel().items().clear();
    tableModel().queryModel().conditions().clear();
  }

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

Common classes

UI

package is.codion.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 java.awt.Dimension;
import java.util.function.Consumer;

import static is.codion.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) {
    this(tableModel, chartDataset, chartTitle, config -> {});
  }

  protected ChartTablePanel(SwingEntityTableModel tableModel, PieDataset<String> chartDataset,
                            String chartTitle, Consumer<Config> config) {
    super(tableModel, config);
    setPreferredSize(new Dimension(200, 200));
    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.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.demos.world.ui;

import is.codion.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.demos.world.model;

import is.codion.common.version.Version;
import is.codion.demos.world.domain.api.World.Lookup;
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.parse(WorldAppModel.class, "/version.properties");

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

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

    addEntityModels(countryModel, lookupModel, continentModel);
  }
}

UI

WorldAppPanel

package is.codion.demos.world.ui;

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.demos.world.domain.api.World;
import is.codion.demos.world.domain.api.World.Continent;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.demos.world.domain.api.World.Lookup;
import is.codion.demos.world.model.ContinentModel;
import is.codion.demos.world.model.CountryModel;
import is.codion.demos.world.model.WorldAppModel;
import is.codion.swing.common.ui.component.table.FilterTableCellRenderer;
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.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 applicationModel) {
    super(applicationModel);
    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);

    ContinentModel 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(Locale.of("en", "EN"));
    Arrays.stream(FlatAllIJThemes.INFOS)
            .forEach(LookAndFeelProvider::addLookAndFeel);
    EntityPanel.Config.TOOLBAR_CONTROLS.set(true);
    FilterTableCellRenderer.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)
            .defaultLoginUser(User.parse("scott:tiger"))
            .start();
  }
}

Domain unit test

package is.codion.demos.world.domain;

import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.demos.world.domain.api.World.CountryLanguage;
import is.codion.demos.world.domain.api.World.Lookup;
import is.codion.framework.db.EntityConnection;
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.test.DefaultEntityFactory;
import is.codion.framework.domain.test.DomainTest;

import org.junit.jupiter.api.Test;

import java.util.Optional;

public final class WorldImplTest extends DomainTest {

  public WorldImplTest() {
    super(new WorldImpl(), WorldEntityFactory::new);
  }

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

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

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

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

  private static final class WorldEntityFactory extends DefaultEntityFactory {

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

    @Override
    public Entity entity(EntityType entityType) {
      Entity entity = super.entity(entityType);
      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
    public void modify(Entity entity) {
      super.modify(entity);
      if (entity.entityType().equals(Country.TYPE)) {
        entity.put(Country.CONTINENT, "Europe");
      }
      else if (entity.entityType().equals(City.TYPE)) {
        entity.put(City.LOCATION, null);
      }
    }

    @Override
    public Optional<Entity> entity(ForeignKey foreignKey) {
      if (foreignKey.referencedType().equals(Country.TYPE)) {
        return Optional.of(entities().builder(Country.TYPE)
                .with(Country.CODE, "ISL")
                .build());
      }
      if (foreignKey.referencedType().equals(City.TYPE)) {
        return Optional.of(entities().builder(City.TYPE)
                .with(City.ID, 1449)
                .build());
      }

      return super.entity(foreignKey);
    }
  }
}

Module Info

Domain

API

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

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

Implementation

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

  exports is.codion.demos.world.domain;

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

Client

/**
 * Client.
 */
module is.codion.demos.world.client {
  requires is.codion.framework.json.domain;
  requires is.codion.swing.framework.ui;
  requires is.codion.plugin.jasperreports;
  requires is.codion.demos.world.domain.api;
  requires com.formdev.flatlaf.intellijthemes;
  requires org.kordamp.ikonli.foundation;
  requires org.jfree.jfreechart;
  requires net.sf.jasperreports.core;
  requires net.sf.jasperreports.pdf;
  requires net.sf.jasperreports.fonts;
  requires org.apache.commons.logging;
  requires com.github.librepdf.openpdf;
  requires org.jxmapviewer.jxmapviewer2;
  requires org.json;

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

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