1. Screenshots

Country & Cities (Table)
country cities table
Country & Cities (Chart)
country cities chart
Country & Languages (Table)
country language table
Country & Languages (Chart)
country language chart
Continent
continent
Lookup
lookup

2. Domain

2.1. API

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

import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.Attribute;
import is.codion.framework.domain.entity.ColorProvider;
import is.codion.framework.domain.entity.DefaultEntityValidator;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.ForeignKey;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.framework.domain.property.DerivedProperty;
import is.codion.framework.domain.property.DerivedProperty.SourceValues;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Objects;

import static is.codion.common.Util.notNull;

/**
 * World domain api.
 */
public interface World {

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

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

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

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

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

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

    default boolean isCapital() {
      return Objects.equals(get(City.ID), country().get(Country.CAPITAL));
    }
  }

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

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

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

    String name();
    String continent();
    String region();
    double surfacearea();
    int population();
  }

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

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

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

    String language();
    int noOfSpeakers();
  }

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

    Attribute<String> NAME = TYPE.stringAttribute("continent");
    Attribute<Integer> SURFACE_AREA = TYPE.integerAttribute("surface_area");
    Attribute<Long> POPULATION = TYPE.longAttribute("population");
    Attribute<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleAttribute("min_life_expectancy");
    Attribute<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleAttribute("max_life_expectancy");
    Attribute<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerAttribute("min_indep_year");
    Attribute<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerAttribute("max_indep_year");
    Attribute<Double> GNP = TYPE.doubleAttribute("gnp");

    String name();
    int surfaceArea();
    long population();
    double minLifeExpectancy();
    double maxLifeExpectancy();
    double gnp();
  }

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

    Attribute<String> COUNTRY_CODE = TYPE.stringAttribute("country.code");
    Attribute<String> COUNTRY_NAME = TYPE.stringAttribute("country.name");
    Attribute<String> COUNTRY_CONTINENT = TYPE.stringAttribute("country.continent");
    Attribute<String> COUNTRY_REGION = TYPE.stringAttribute("country.region");
    Attribute<Double> COUNTRY_SURFACEAREA = TYPE.doubleAttribute("country.surfacearea");
    Attribute<Integer> COUNTRY_INDEPYEAR = TYPE.integerAttribute("country.indepyear");
    Attribute<Integer> COUNTRY_POPULATION = TYPE.integerAttribute("country.population");
    Attribute<Double> COUNTRY_LIFEEXPECTANCY = TYPE.doubleAttribute("country.lifeexpectancy");
    Attribute<Double> COUNTRY_GNP = TYPE.doubleAttribute("country.gnp");
    Attribute<Double> COUNTRY_GNPOLD = TYPE.doubleAttribute("country.gnpold");
    Attribute<String> COUNTRY_LOCALNAME = TYPE.stringAttribute("country.localname");
    Attribute<String> COUNTRY_GOVERNMENTFORM = TYPE.stringAttribute("country.governmentform");
    Attribute<String> COUNTRY_HEADOFSTATE = TYPE.stringAttribute("country.headofstate");
    Attribute<String> COUNTRY_CODE2 = TYPE.stringAttribute("country.code2");
    Attribute<byte[]> COUNTRY_FLAG = TYPE.byteArrayAttribute("country.flag");
    Attribute<Integer> CITY_ID = TYPE.integerAttribute("city.id");
    Attribute<String> CITY_NAME = TYPE.stringAttribute("city.name");
    Attribute<String> CITY_DISTRICT = TYPE.stringAttribute("city.district");
    Attribute<Integer> CITY_POPULATION = TYPE.integerAttribute("city.population");
  }

  final class Location implements Serializable {

    private static final long serialVersionUID = 1;

    private final double latitude;
    private final double longitude;

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

    public double latitude() {
      return latitude;
    }

    public double longitude() {
      return longitude;
    }

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

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

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

    @Override
    public int hashCode() {
      return Objects.hash(latitude, longitude);
    }
  }

  final class CityColorProvider implements ColorProvider {

    private static final long serialVersionUID = 1;

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

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

      return null;
    }
  }

  final class CityValidator extends DefaultEntityValidator implements Serializable {

    private static final long serialVersionUID = 1;

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

  final class NoOfSpeakersProvider implements DerivedProperty.Provider<Integer> {

    private static final long serialVersionUID = 1;

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

      return null;
    }
  }

  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;
    }
  }
}
public final class WorldObjectMapperFactory extends DefaultEntityObjectMapperFactory {

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

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

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

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

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

    return objectMapper;
  }
}

Exposing the WorldObjectMapperFactory for the ServiceLoader.

src/main/java/module-info.java

  provides is.codion.plugin.jackson.json.domain.EntityObjectMapperFactory
          with is.codion.framework.demos.world.domain.api.WorldObjectMapperFactory;

2.2. Implementation

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

import is.codion.common.item.Item;
import is.codion.framework.demos.world.domain.api.World;
import is.codion.framework.domain.DefaultDomain;
import is.codion.framework.domain.entity.OrderBy;
import is.codion.framework.domain.entity.query.SelectQuery;
import is.codion.framework.domain.property.ColumnProperty.ValueConverter;

import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

import static is.codion.common.item.Item.item;
import static is.codion.framework.domain.entity.EntityDefinition.definition;
import static is.codion.framework.domain.entity.KeyGenerator.sequence;
import static is.codion.framework.domain.entity.OrderBy.ascending;
import static is.codion.framework.domain.property.Property.*;
import static java.lang.Double.parseDouble;
import static java.util.Arrays.asList;

public final class WorldImpl extends DefaultDomain implements World {

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

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

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

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

  void country() {
    add(definition(
            primaryKeyProperty(Country.CODE, "Code")
                    .updatable(true)
                    .maximumLength(3),
            columnProperty(Country.NAME, "Name")
                    .searchProperty(true)
                    .nullable(false)
                    .maximumLength(52),
            itemProperty(Country.CONTINENT, "Continent", CONTINENT_ITEMS)
                    .nullable(false),
            columnProperty(Country.REGION, "Region")
                    .nullable(false)
                    .maximumLength(26),
            columnProperty(Country.SURFACEAREA, "Surface area")
                    .nullable(false)
                    .numberFormatGrouping(true)
                    .maximumFractionDigits(2),
            columnProperty(Country.INDEPYEAR, "Indep. year")
                    .valueRange(-2000, 2500),
            columnProperty(Country.INDEPYEAR_SEARCHABLE)
                    .columnExpression("to_char(indepyear)")
                    .searchProperty(true)
                    .readOnly(true),
            columnProperty(Country.POPULATION, "Population")
                    .nullable(false)
                    .numberFormatGrouping(true),
            columnProperty(Country.LIFE_EXPECTANCY, "Life expectancy")
                    .maximumFractionDigits(1)
                    .valueRange(0, 99),
            columnProperty(Country.GNP, "GNP")
                    .numberFormatGrouping(true)
                    .maximumFractionDigits(2),
            columnProperty(Country.GNPOLD, "GNP old")
                    .numberFormatGrouping(true)
                    .maximumFractionDigits(2),
            columnProperty(Country.LOCALNAME, "Local name")
                    .nullable(false)
                    .maximumLength(45),
            columnProperty(Country.GOVERNMENTFORM, "Government form")
                    .nullable(false),
            columnProperty(Country.HEADOFSTATE, "Head of state")
                    .maximumLength(60),
            columnProperty(Country.CAPITAL),
            foreignKeyProperty(Country.CAPITAL_FK, "Capital"),
            denormalizedViewProperty(Country.CAPITAL_POPULATION, "Capital pop.",
                    Country.CAPITAL_FK, City.POPULATION)
                    .numberFormatGrouping(true),
            subqueryProperty(Country.NO_OF_CITIES, "No. of cities",
                    "select count(*) from world.city " +
                            "where city.countrycode = country.code"),
            subqueryProperty(Country.NO_OF_LANGUAGES, "No. of languages",
                    "select count(*) from world.countrylanguage " +
                            "where countrylanguage.countrycode = country.code"),
            blobProperty(Country.FLAG, "Flag")
                    .eagerlyLoaded(true),
            columnProperty(Country.CODE_2, "Code 2")
                    .nullable(false)
                    .maximumLength(2))
            .orderBy(ascending(Country.NAME))
            .stringFactory(Country.NAME)
            .description("Countries of the World")
            .caption("Country"));
  }

  void countryLanguage() {
    add(definition(
            columnProperty(CountryLanguage.COUNTRY_CODE)
                    .primaryKeyIndex(0)
                    .updatable(true),
            columnProperty(CountryLanguage.LANGUAGE, "Language")
                    .primaryKeyIndex(1)
                    .updatable(true),
            foreignKeyProperty(CountryLanguage.COUNTRY_FK, "Country"),
            columnProperty(CountryLanguage.IS_OFFICIAL, "Official")
                    .columnHasDefaultValue(true)
                    .nullable(false),
            derivedProperty(CountryLanguage.NO_OF_SPEAKERS, "No. of speakers",
                    new NoOfSpeakersProvider(), CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
                    .numberFormatGrouping(true),
            columnProperty(CountryLanguage.PERCENTAGE, "Percentage")
                    .nullable(false)
                    .maximumFractionDigits(1)
                    .valueRange(0, 100))
            .orderBy(OrderBy.builder()
                    .ascending(CountryLanguage.LANGUAGE)
                    .descending(CountryLanguage.PERCENTAGE)
                    .build())
            .description("Languages")
            .caption("Language"));
  }

  void lookup() {
    add(definition(
            columnProperty(Lookup.COUNTRY_CODE, "Country code")
                    .primaryKeyIndex(0),
            columnProperty(Lookup.COUNTRY_NAME, "Country name"),
            itemProperty(Lookup.COUNTRY_CONTINENT, "Continent", CONTINENT_ITEMS),
            columnProperty(Lookup.COUNTRY_REGION, "Region"),
            columnProperty(Lookup.COUNTRY_SURFACEAREA, "Surface area")
                    .numberFormatGrouping(true),
            columnProperty(Lookup.COUNTRY_INDEPYEAR, "Indep. year"),
            columnProperty(Lookup.COUNTRY_POPULATION, "Country population")
                    .numberFormatGrouping(true),
            columnProperty(Lookup.COUNTRY_LIFEEXPECTANCY, "Life expectancy"),
            columnProperty(Lookup.COUNTRY_GNP, "GNP")
                    .numberFormatGrouping(true),
            columnProperty(Lookup.COUNTRY_GNPOLD, "GNP old")
                    .numberFormatGrouping(true),
            columnProperty(Lookup.COUNTRY_LOCALNAME, "Local name"),
            columnProperty(Lookup.COUNTRY_GOVERNMENTFORM, "Government form"),
            columnProperty(Lookup.COUNTRY_HEADOFSTATE, "Head of state"),
            blobProperty(Lookup.COUNTRY_FLAG, "Flag"),
            columnProperty(Lookup.COUNTRY_CODE2, "Code2"),
            columnProperty(Lookup.CITY_ID)
                    .primaryKeyIndex(1),
            columnProperty(Lookup.CITY_NAME, "City"),
            columnProperty(Lookup.CITY_DISTRICT, "District"),
            columnProperty(Lookup.CITY_POPULATION, "City population")
                    .numberFormatGrouping(true))
            .selectQuery(SelectQuery.builder()
                    .from("world.country join world.city on city.countrycode = country.code")
                    .build())
            .orderBy(OrderBy.builder()
                    .ascending(Lookup.COUNTRY_NAME)
                    .descending(Lookup.CITY_POPULATION)
                    .build())
            .readOnly(true)
            .description("Lookup country or city")
            .caption("Lookup"));
  }

  void continent() {
    add(definition(
            columnProperty(Continent.NAME, "Continent")
                    .groupingColumn(true)
                    .beanProperty("name"),
            columnProperty(Continent.SURFACE_AREA, "Surface area")
                    .columnExpression("sum(surfacearea)")
                    .aggregateColumn(true)
                    .numberFormatGrouping(true),
            columnProperty(Continent.POPULATION, "Population")
                    .columnExpression("sum(population)")
                    .aggregateColumn(true)
                    .numberFormatGrouping(true),
            columnProperty(Continent.MIN_LIFE_EXPECTANCY, "Min. life expectancy")
                    .columnExpression("min(lifeexpectancy)")
                    .aggregateColumn(true),
            columnProperty(Continent.MAX_LIFE_EXPECTANCY, "Max. life expectancy")
                    .columnExpression("max(lifeexpectancy)")
                    .aggregateColumn(true),
            columnProperty(Continent.MIN_INDEPENDENCE_YEAR, "Min. ind. year")
                    .columnExpression("min(indepyear)")
                    .aggregateColumn(true),
            columnProperty(Continent.MAX_INDEPENDENCE_YEAR, "Max. ind. year")
                    .columnExpression("max(indepyear)")
                    .aggregateColumn(true),
            columnProperty(Continent.GNP, "GNP")
                    .columnExpression("sum(gnp)")
                    .aggregateColumn(true)
                    .numberFormatGrouping(true))
            .tableName("world.country")
            .readOnly(true)
            .description("Continents of the World")
            .caption("Continent"));
  }

  private static final class LocationConverter implements ValueConverter<Location, String> {

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

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

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

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

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

2.3. Domain unit test

/*
 * Copyright (c) 2004 - 2022, Björn Darri Sigurðsson. All Rights Reserved.
 */
package is.codion.framework.demos.world.domain;

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

import org.junit.jupiter.api.Test;

import java.util.Map;

public final class WorldImplTest extends EntityTestUnit {

  public WorldImplTest() {
    super(WorldImpl.class.getName());
  }

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

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

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

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

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

    return entity;
  }

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

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

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

3. Model

3.1. Main application model

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

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

public final class WorldAppModel extends SwingEntityApplicationModel {

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

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

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

    addEntityModels(countryModel, lookupModel, continentModel);
  }
}
package is.codion.framework.demos.world.model;

import is.codion.common.value.Value;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.model.SwingEntityTableModel;

public final class CountryModel extends SwingEntityModel {

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

  CountryModel(EntityConnectionProvider connectionProvider) {
    super(new CountryTableModel(connectionProvider));
    editModel().initializeComboBoxModels(Country.CAPITAL_FK);

    SwingEntityModel cityModel = new SwingEntityModel(new CityTableModel(connectionProvider));
    cityModel.editModel().initializeComboBoxModels(City.COUNTRY_FK);
    SwingEntityModel countryLanguageModel = new SwingEntityModel(new CountryLanguageTableModel(connectionProvider));
    countryLanguageModel.editModel().initializeComboBoxModels(CountryLanguage.COUNTRY_FK);

    addDetailModels(cityModel, countryLanguageModel);

    cityModel.tableModel().addRefreshListener(() ->
            averageCityPopulationValue.set(averageCityPopulation()));
    CountryEditModel countryEditModel = (CountryEditModel) editModel();
    countryEditModel.setAverageCityPopulationObserver(averageCityPopulationValue.observer());
  }

  private Double averageCityPopulation() {
    if (editModel().isEntityNew()) {
      return null;
    }

    SwingEntityTableModel cityTableModel = detailModel(City.TYPE).tableModel();
    Entity country = editModel().entityCopy();

    return Entity.castTo(City.class, cityTableModel.items()).stream()
            .filter(city -> city.isInCountry(country))
            .map(City::population)
            .mapToInt(Integer::valueOf)
            .average()
            .orElse(0d);
  }
}
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.report.ReportException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.condition.Condition;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.model.EntitySearchModelConditionModel;
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.model.JasperReports.classPathReport;
import static is.codion.plugin.jasperreports.model.JasperReports.fillReport;
import static java.util.Collections.singletonMap;

public final class CountryTableModel extends SwingEntityTableModel {

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

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

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

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

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

  private void configureCapitalConditionModel() {
    ((EntitySearchModelConditionModel) tableConditionModel()
            .conditionModel(Country.CAPITAL_FK))
            .entitySearchModel()
            .setAdditionalConditionSupplier(new CapitalConditionSupplier());
  }

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

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.plugin.jasperreports.model.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.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import static is.codion.framework.db.condition.Condition.where;
import static is.codion.framework.domain.entity.OrderBy.descending;

public final class CountryReportDataSource extends JasperReportsDataSource<Entity> {

  private final EntityConnection connection;

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

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

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

  private static final class CountryValueProvider implements BiFunction<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 entity, JRField field) {
      Country country = entity.castTo(Country.class);
      switch (field.getName()) {
        case NAME: return country.name();
        case CONTINENT: return country.continent();
        case REGION: return country.region();
        case SURFACEAREA: return country.surfacearea();
        case POPULATION: return 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 entity, JRField field) {
      City city = entity.castTo(City.class);
      switch (field.getName()) {
        case NAME: return city.name();
        case POPULATION: return 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 final int noOfCountries;

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

    @Override
    public void accept(Entity country) {
      progressReporter.publish(country.get(Country.NAME));
      progressReporter.setProgress(100 * counter.incrementAndGet() / noOfCountries);
    }
  }
}
package is.codion.framework.demos.world.model;

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

public final class CountryEditModel extends SwingEntityEditModel {

  private ValueObserver<Double> averageCityPopulationObserver;

  CountryEditModel(EntityConnectionProvider connectionProvider) {
    super(Country.TYPE, connectionProvider);
  }

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

    return comboBoxModel;
  }

  public ValueObserver<Double> averageCityPopulationValue() {
    return averageCityPopulationObserver;
  }

  public void setAverageCityPopulationObserver(ValueObserver<Double> averageCityPopulationObserver) {
    this.averageCityPopulationObserver = averageCityPopulationObserver;
  }
}
package is.codion.framework.demos.world.model;

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

import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
import 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.ArrayList;
import java.util.Collection;
import java.util.Iterator;

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

public final class CityTableModel extends SwingEntityTableModel {

  public static final String OPENSTREETMAP_ORG_SEARCH = "https://nominatim.openstreetmap.org/search/";

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

  CityTableModel(EntityConnectionProvider connectionProvider) {
    super(City.TYPE, connectionProvider);
    selectionModel().addSelectedItemsListener(displayLocationEvent::onEvent);
    selectionModel().addSelectionListener(this::updateCitiesWithoutLocationSelected);
    addRefreshListener(this::refreshChartDataset);
  }

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

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

  public StateObserver citiesWithoutLocationSelectedObserver() {
    return citiesWithoutLocationSelectedState.observer();
  }

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

  private void fetchLocation(City city) throws IOException, DatabaseException, ValidationException {
    JSONArray jsonArray = toJSONArray(new URL(OPENSTREETMAP_ORG_SEARCH +
            URLEncoder.encode(city.name(), UTF_8.name()) + "," +
            URLEncoder.encode(city.country().name(), UTF_8.name()) + "?format=json"));

    if (jsonArray.length() > 0) {
      fetchLocation(city, (JSONObject) jsonArray.get(0));
    }
  }

  private void fetchLocation(City city, JSONObject cityInformation) throws DatabaseException, ValidationException {
    city.location(new Location(cityInformation.getDouble("lat"), cityInformation.getDouble("lon")));
    editModel().update(singletonList(city));
  }

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

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

  private void updateCitiesWithoutLocationSelected() {
    citiesWithoutLocationSelectedState.set(selectionModel().getSelectedItems().stream()
            .anyMatch(city -> city.isNull(City.LOCATION)));
  }
}
package is.codion.framework.demos.world.model;

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

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

public final class CountryLanguageTableModel extends SwingEntityTableModel {

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

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

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

  private void refreshChartDataset() {
    chartDataset.clear();
    Entity.castTo(CountryLanguage.class, visibleItems()).forEach(language ->
            chartDataset.setValue(language.language(), language.noOfSpeakers()));
  }
}
package is.codion.framework.demos.world.model;

import is.codion.common.model.table.ColumnConditionModel;
import is.codion.common.model.table.ColumnConditionModel.AutomaticWildcard;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingDetailModelHandler;
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;
import java.util.List;

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().addRefreshListener(this::refreshChartDatasets);
    CountryModel countryModel = new CountryModel(connectionProvider);
    addDetailModel(new CountryModelHandler(countryModel)).setActive(true);
  }

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

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

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

  public CategoryDataset lifeExpectancyDataset() {
    return lifeExpectancyDataset;
  }

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

  private static final class CountryModel extends SwingEntityModel {

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

  private static final class CountryModelHandler extends SwingDetailModelHandler {

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

    @Override
    public void onSelection(List<Entity> selectedEntities) {
      Collection<String> continentNames = Entity.get(Continent.NAME, selectedEntities);
      if (detailModel().tableModel().tableConditionModel().setEqualConditionValues(Country.CONTINENT, continentNames)) {
        detailModel().tableModel().refresh();
      }
    }
  }
}
package is.codion.framework.demos.world.model;

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

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import static java.util.Collections.singletonList;

public final class LookupTableModel extends SwingEntityTableModel {

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

  public void exportCSV(File file) throws IOException {
    Files.write(file.toPath(), singletonList(tableDataAsDelimitedString(',')));
  }
}

4. UI

4.1. Main application panel

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

import is.codion.common.model.CancelException;
import is.codion.common.model.table.ColumnConditionModel;
import is.codion.common.model.table.ColumnConditionModel.AutomaticWildcard;
import is.codion.common.user.User;
import is.codion.common.version.Version;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.demos.world.model.CountryModel;
import is.codion.framework.demos.world.model.WorldAppModel;
import is.codion.swing.common.ui.laf.LookAndFeelSelectionPanel;
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.EntityTableCellRenderer;
import is.codion.swing.framework.ui.ReferentialIntegrityErrorHandling;

import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes;

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

import static is.codion.swing.common.ui.laf.LookAndFeelProvider.addLookAndFeelProvider;
import static is.codion.swing.common.ui.laf.LookAndFeelProvider.lookAndFeelProvider;
import static java.util.Arrays.asList;

public final class WorldAppPanel extends EntityApplicationPanel<WorldAppModel> {

  public WorldAppPanel() {
    super("World");
  }

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

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

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

    return asList(countryPanel, continentPanel, lookupPanel);
  }

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

  @Override
  protected Version clientVersion() {
    return WorldAppModel.VERSION;
  }

  @Override
  protected String defaultLookAndFeelName() {
    return FlatDarkLaf.class.getName();
  }

  public static void main(String[] args) throws CancelException {
    Locale.setDefault(new Locale("en", "EN"));
    Arrays.stream(FlatAllIJThemes.INFOS).forEach(themeInfo ->
            addLookAndFeelProvider(lookAndFeelProvider(themeInfo.getClassName())));
    LookAndFeelSelectionPanel.CHANGE_ON_SELECTION.set(true);
    ColumnConditionModel.AUTOMATIC_WILDCARD.set(AutomaticWildcard.PREFIX_AND_POSTFIX);
    ColumnConditionModel.CASE_SENSITIVE.set(false);
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    EntityTableCellRenderer.NUMERICAL_HORIZONTAL_ALIGNMENT.set(SwingConstants.CENTER);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING
            .set(ReferentialIntegrityErrorHandling.DISPLAY_DEPENDENCIES);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.world.domain.WorldImpl");
    SwingUtilities.invokeLater(() -> new WorldAppPanel().starter()
            .frameSize(new Dimension(1280, 720))
            .defaultLoginUser(User.parse("scott:tiger"))
            .start());
  }
}
package is.codion.framework.demos.world.ui;

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

final class CountryPanel extends EntityPanel {

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

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

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

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

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

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

import static is.codion.swing.common.ui.component.Components.label;
import static is.codion.swing.common.ui.component.Components.panel;
import static is.codion.swing.common.ui.component.panel.Panels.createEastButtonPanel;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

final class CountryEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(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(120);
    createAttributeComboBox(Country.REGION)
            .preferredWidth(120);
    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);
    createAttributeComboBox(Country.GOVERNMENTFORM)
            .preferredWidth(120)
            .editable(true);
    createTextField(Country.HEADOFSTATE);
    EntityComboBox capitalComboBox = createForeignKeyComboBox(Country.CAPITAL_FK)
            .preferredWidth(120)
            .build();
    //create a panel with a button for adding a new city
    JPanel capitalPanel = createEastButtonPanel(capitalComboBox, EntityPanel.builder(City.TYPE)
            .editPanelClass(CityEditPanel.class)
            .onBuildEditPanel(this::initializeCapitalEditPanel)
            .createEditPanelAction(capitalComboBox));
    //add a field displaying the avarage city population for the selected country
    CountryEditModel editModel = (CountryEditModel) editModel();
    NumberField<Double> averageCityPopulationField = Components.doubleField()
            .linkedValueObserver(editModel.averageCityPopulationValue())
            .maximumFractionDigits(2)
            .groupingUsed(true)
            .horizontalAlignment(SwingConstants.CENTER)
            .focusable(false)
            .editable(false)
            .build();

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

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

    JPanel surfaceAreaIndYear = panel(gridLayout(1, 2))
            .add(createInputPanel(Country.SURFACEAREA))
            .add(createInputPanel(Country.INDEPYEAR))
            .build();

    JPanel populationLifeExpectancyPanel = panel(gridLayout(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, capitalPanel);
    addInputPanel(Country.CONTINENT);
    addInputPanel(Country.REGION);
    add(surfaceAreaIndYear);
    add(populationLifeExpectancyPanel);
    add(gnpPanel);
    addInputPanel(Country.GOVERNMENTFORM);
    addInputPanel(Country.HEADOFSTATE);
    add(createInputPanel(label("Avg. city population")
            .horizontalAlignment(SwingConstants.CENTER)
            .build(), averageCityPopulationField));
  }

  private void initializeCapitalEditPanel(EntityEditPanel capitalEditPanel) {
    //set the country to the one selected in the CountryEditPanel
    Entity country = editModel().entityCopy();
    if (country.primaryKey().isNotNull()) {
      //if a country is selected, then we don't allow it to be changed
      capitalEditPanel.editModel().put(City.COUNTRY_FK, country);
      //initialize the panel components, so we can configure the country component
      capitalEditPanel.initializePanel();
      //disable the country selection component
      JComponent countryComponent = capitalEditPanel.getComponent(City.COUNTRY_FK);
      countryComponent.setEnabled(false);
      countryComponent.setFocusable(false);
      //and change the initial focus property
      capitalEditPanel.setInitialFocusAttribute(City.NAME);
    }
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.report.ReportException;
import is.codion.framework.demos.world.model.CountryTableModel;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
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.icons.FrameworkIcons;

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

import java.awt.Dimension;

final class CountryTablePanel extends EntityTablePanel {

  CountryTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
    setControl(ControlCode.PRINT_TABLE, Control.builder(this::viewCountryReport)
            .enabledState(tableModel.selectionModel().selectionNotEmptyObserver())
            .smallIcon(FrameworkIcons.instance().print())
            .build());
  }

  @Override
  protected Controls createPrintControls() {
    return super.createPrintControls().removeAll();
  }

  private void viewCountryReport() throws Exception {
    Dialogs.progressWorkerDialog(this::fillCountryReport)
            .owner(this)
            .indeterminate(false)
            .stringPainted(true)
            .onResult(this::viewReport)
            .execute();
  }

  private JasperPrint fillCountryReport(ProgressReporter<String> progressReporter) throws DatabaseException, ReportException {
    return ((CountryTableModel) tableModel()).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();
  }
}
package is.codion.framework.demos.world.ui;

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

import java.util.List;

final class CityTablePanel extends ChartTablePanel {

  CityTablePanel(CityTableModel tableModel) {
    super(tableModel, tableModel.chartDataset(), "Cities");
  }

  @Override
  protected Controls createPopupControls(List<Controls> additionalPopupControls) {
    return super.createPopupControls(additionalPopupControls)
            .addAt(0, createFetchLocationControl())
            .addSeparatorAt(1);
  }

  private Control createFetchLocationControl() {
    return Control.builder(this::fetchLocation)
            .caption("Fetch location")
            .enabledState(((CityTableModel) tableModel()).citiesWithoutLocationSelectedObserver())
            .build();
  }

  private void fetchLocation() {
    FetchLocationTask fetchLocationTask = new FetchLocationTask(((CityTableModel) tableModel()));

    Dialogs.progressWorkerDialog(fetchLocationTask)
            .owner(this)
            .title("Fetching locations")
            .stringPainted(true)
            .controls(Controls.builder()
                    .control(Control.builder(fetchLocationTask::cancel)
                            .caption("Cancel")
                            .enabledState(fetchLocationTask.isWorkingObserver()))
                    .build())
            .onException(this::displayFetchException)
            .execute();
  }

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

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

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

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

    @Override
    public Void perform(ProgressReporter<String> progressReporter) throws Exception {
      tableModel.fetchLocationForSelected(progressReporter, cancelledState);
      return null;
    }

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

    private StateObserver isWorkingObserver() {
      return cancelledState.reversedObserver();
    }
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.common.event.EventDataListener;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Location;
import is.codion.framework.demos.world.model.CityTableModel;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.ui.component.Components;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

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.JPanel;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;

import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;
import static java.util.stream.Collectors.toSet;

public final class CityEditPanel extends EntityEditPanel {

  private static final int MIN_ZOOM = 19;

  private final JXMapKit mapKit;

  public CityEditPanel(SwingEntityEditModel editModel) {
    this(editModel, null);
  }

  CityEditPanel(SwingEntityEditModel editModel, CityTableModel tableModel) {
    super(editModel);
    this.mapKit = tableModel == null ? null : createMapKit(tableModel);
  }

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

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

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

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

  private static JXMapKit createMapKit(CityTableModel tableModel) {
    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<>());

    tableModel.addDisplayLocationListener(new DisplayLocationListener(mapKit.getMainMap()));

    return mapKit;
  }

  private static final class DisplayLocationListener implements EventDataListener<Collection<Entity>> {

    private static final int SINGLE_WAYPOINT_ZOOM_LEVEL = 15;

    private final JXMapViewer mapViewer;

    private DisplayLocationListener(JXMapViewer mapViewer) {
      this.mapViewer = mapViewer;
    }

    @Override
    public void onEvent(Collection<Entity> cities) {
      SwingUtilities.invokeLater(() -> paintWaypoints(cities));
    }

    private void paintWaypoints(Collection<Entity> cities) {
      paintWaypoints(cities.stream()
              .map(city -> city.get(City.LOCATION))
              .filter(Objects::nonNull)
              .collect(toSet()));
    }

    private void paintWaypoints(Set<Location> positions) {
      Set<GeoPosition> geoPositions = positions.stream()
              .map(DisplayLocationListener::toGeoPosition)
              .collect(toSet());
      WaypointPainter<Waypoint> overlayPainter = (WaypointPainter<Waypoint>) mapViewer.getOverlayPainter();
      overlayPainter.setWaypoints(geoPositions.stream()
              .map(DefaultWaypoint::new)
              .collect(toSet()));
      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);
      }
    }

    private static GeoPosition toGeoPosition(Location location) {
      return new GeoPosition(location.latitude(), location.longitude());
    }
  }
}
package is.codion.framework.demos.world.ui;

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

final class CountryLanguageTablePanel extends ChartTablePanel {

  CountryLanguageTablePanel(CountryLanguageTableModel tableModel) {
    super(tableModel, tableModel.chartDataset(), "Languages");
  }
}
package is.codion.framework.demos.world.ui;

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

import javax.swing.JPanel;

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

final class CountryLanguageEditPanel extends EntityEditPanel {

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

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

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

    JPanel percentageOfficialPanel = panel(gridLayout(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);
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.model.ContinentModel;
import is.codion.swing.common.ui.component.Components;
import is.codion.swing.framework.model.SwingEntityModel;
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 java.awt.event.KeyEvent;

import static is.codion.framework.demos.world.ui.ChartPanels.createBarChartPanel;
import static is.codion.framework.demos.world.ui.ChartPanels.createPieChartPanel;
import static is.codion.swing.common.ui.Sizes.setPreferredHeight;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

final class ContinentPanel extends EntityPanel {

  private final EntityPanel countryPanel;

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

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

    EntityTablePanel tablePanel = tablePanel();
    tablePanel.initializePanel();
    setPreferredHeight(tablePanel, 200);

    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");
    lifeExpectancyChartPanel.setPreferredSize(new Dimension(lifeExpectancyChartPanel.getPreferredSize().width, 120));

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

    JPanel pieChartChartPanel = Components.panel(gridLayout(1, 3))
            .add(populationChartPanel)
            .add(surfaceAreaChartPanel)
            .add(gnpChartPanel)
            .build();

    JPanel chartPanel = Components.panel(borderLayout())
            .add(lifeExpectancyChartPanel, BorderLayout.NORTH)
            .add(pieChartChartPanel, BorderLayout.CENTER)
            .build();

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

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

    setLayout(borderLayout());

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

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

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

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

final class ContinentTablePanel extends EntityTablePanel {

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

  @Override
  protected Controls createPopupControls(List<Controls> additionalPopupControls) {
    return Controls.builder()
            .control(createRefreshControl())
            .build();
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.model.LookupTableModel;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import is.codion.swing.framework.ui.icons.FrameworkIcons;

import javax.swing.JTable;
import java.io.File;
import java.io.IOException;
import java.util.List;

final class LookupTablePanel extends EntityTablePanel {

  LookupTablePanel(SwingEntityTableModel lookupModel) {
    super(lookupModel);
    table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    setConditionPanelVisible(true);
    setShowRefreshingProgressBar(true);
    setControl(ControlCode.CLEAR, Control.builder(this::clearTableAndConditions)
            .caption("Clear")
            .mnemonic('C')
            .smallIcon(FrameworkIcons.instance().clear())
            .build());
  }

  @Override
  protected Controls createPopupControls(List<Controls> additionalPopupControls) {
    return super.createPopupControls(additionalPopupControls)
            .addSeparatorAt(2)
            .addAt(3, Control.builder(this::exportCSV)
                    .caption("Export CSV...")
                    .build());
  }

  private void exportCSV() throws IOException {
    File fileToSave = Dialogs.fileSelectionDialog()
            .owner(this)
            .selectFileToSave("export.csv");
    Dialogs.progressWorkerDialog(() -> ((LookupTableModel) tableModel()).exportCSV(fileToSave))
            .owner(this)
            .title("Exporting data")
            .onResult("Export successful")
            .onException("Export failed")
            .execute();
  }

  private void clearTableAndConditions() {
    tableModel().clear();
    tableModel().tableConditionModel().clearConditions();
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.swing.common.ui.component.Components;
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.JPanel;
import javax.swing.JTabbedPane;
import java.awt.BorderLayout;
import java.awt.event.KeyEvent;

import static is.codion.framework.demos.world.ui.ChartPanels.createPieChartPanel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;

abstract class ChartTablePanel extends EntityTablePanel {

  private final ChartPanel chartPanel;

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

  @Override
  protected final void layoutPanel(JPanel tablePanel, JPanel southPanel) {
    JPanel tableViewPanel = Components.panel(borderLayout())
            .add(tablePanel, BorderLayout.CENTER)
            .add(southPanel, BorderLayout.SOUTH)
            .build();
    JTabbedPane tabbedPane = Components.tabbedPane()
            .tabBuilder("Table", tableViewPanel)
            .mnemonic(KeyEvent.VK_1)
            .add()
            .tabBuilder("Chart", chartPanel)
            .mnemonic(KeyEvent.VK_2)
            .add()
            .build();
    setLayout(borderLayout());
    add(tabbedPane, BorderLayout.CENTER);
  }
}
package is.codion.framework.demos.world.ui;

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

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

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

final class ChartPanels {

  private ChartPanels() {}

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

    return new ChartPanel(chart);
  }

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

    return new ChartPanel(chart);
  }

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

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