1. Screenshots

Country
country
Custom Country Tables
custom country tables
Custom Country Charts
custom country charts
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.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.ForeignKey;

import org.jxmapviewer.viewer.GeoPosition;

import java.util.Objects;

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

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

  interface City extends Entity {
    EntityType<City> 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<GeoPosition> LOCATION = TYPE.attribute("location", GeoPosition.class);

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

    String name();
    Integer population();

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

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

  interface Country {
    EntityType<Entity> TYPE = DOMAIN.entityType("world.country");

    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<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", Country.CAPITAL, City.ID);
  }

  interface CountryLanguage extends Entity {
    EntityType<CountryLanguage> 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", CountryLanguage.COUNTRY_CODE, Country.CODE);

    String language();
    Integer noOfSpeakers();
  }

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

    Attribute<String> NAME = TYPE.stringAttribute("continent");
    Attribute<Integer> SURFACE_AREA = TYPE.integerAttribute("sum(surfacearea)");
    Attribute<Long> POPULATION = TYPE.longAttribute("sum(population)");
    Attribute<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleAttribute("min(lifeexpectancy)");
    Attribute<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleAttribute("max(lifeexpectancy)");
    Attribute<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerAttribute("min(indepyear)");
    Attribute<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerAttribute("max(indepyear)");
    Attribute<Double> GNP = TYPE.doubleAttribute("sum(gnp)");

    String name();
    Integer surfaceArea();
    Long population();
    Double minLifeExpectancy();
    Double maxLifeExpectancy();
    Double gnp();
  }

  interface Lookup {
    EntityType<Entity> 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");
  }
}

2.2. Implementation

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

import is.codion.framework.demos.world.domain.api.World;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.domain.DefaultDomain;
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.EntityDefinition;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.framework.domain.entity.query.SelectQuery;
import is.codion.framework.domain.property.ColumnProperty.ValueConverter;
import is.codion.framework.domain.property.DerivedProperty;

import org.jxmapviewer.viewer.GeoPosition;

import java.awt.Color;
import java.io.Serializable;
import java.sql.SQLException;
import java.sql.Statement;

import static is.codion.common.Util.notNull;
import static is.codion.common.item.Item.item;
import static is.codion.framework.domain.entity.KeyGenerator.sequence;
import static is.codion.framework.domain.entity.OrderBy.orderBy;
import static is.codion.framework.domain.entity.StringFactory.stringFactory;
import static is.codion.framework.domain.property.Properties.*;
import static java.lang.Double.parseDouble;
import static java.util.Arrays.asList;

public final class WorldImpl extends DefaultDomain {

  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() {
    define(City.TYPE,
            primaryKeyProperty(City.ID),
            columnProperty(City.NAME, "Name")
                    .searchProperty()
                    .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()))
            .keyGenerator(sequence("world.city_seq"))
            .validator(new CityValidator())
            .orderBy(orderBy().ascending(City.NAME))
            .stringFactory(stringFactory(City.NAME))
            .colorProvider(new CityColorProvider())
            .caption("City");
  }

  void country() {
    define(Country.TYPE,
            primaryKeyProperty(Country.CODE, "Country code")
                    .updatable(true)
                    .maximumLength(3),
            columnProperty(Country.NAME, "Name")
                    .searchProperty()
                    .nullable(false)
                    .maximumLength(52),
            itemProperty(Country.CONTINENT, "Continent", asList(
                    item("Africa"), item("Antarctica"), item("Asia"),
                    item("Europe"), item("North America"), item("Oceania"),
                    item("South America")))
                    .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")
                    .minimumValue(-2000).maximumValue(2500),
            columnProperty(Country.POPULATION, "Population")
                    .nullable(false)
                    .numberFormatGrouping(true),
            columnProperty(Country.LIFE_EXPECTANCY, "Life expectancy")
                    .maximumFractionDigits(1)
                    .minimumValue(0).maximumValue(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(),
            columnProperty(Country.CODE_2, "Code2")
                    .nullable(false)
                    .maximumLength(2))
            .orderBy(orderBy().ascending(Country.NAME))
            .stringFactory(stringFactory(Country.NAME))
            .caption("Country");
  }

  void countryLanguage() {
    define(CountryLanguage.TYPE,
            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, "Is official")
                    .columnHasDefaultValue()
                    .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)
                    .minimumValue(0).maximumValue(100))
            .orderBy(orderBy().ascending(CountryLanguage.LANGUAGE).descending(CountryLanguage.PERCENTAGE))
            .caption("Language");
  }

  void lookup() {
    define(Lookup.TYPE,
            columnProperty(Lookup.COUNTRY_CODE, "Country code"),
            columnProperty(Lookup.COUNTRY_NAME, "Country name"),
            columnProperty(Lookup.COUNTRY_CONTINENT, "Continent"),
            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),
            columnProperty(Lookup.CITY_NAME, "City"),
            columnProperty(Lookup.CITY_DISTRICT, "District"),
            columnProperty(Lookup.CITY_POPULATION, "City population")
                    .numberFormatGrouping(true))
            .selectQuery(SelectQuery.builder()
                    .fromClause("world.country join world.city on city.countrycode = country.code")
                    .build())
            .orderBy(orderBy().ascending(Lookup.COUNTRY_NAME).descending(Lookup.CITY_POPULATION))
            .readOnly()
            .caption("Lookup");
  }

  void continent() {
    define(Continent.TYPE, "world.country",
            columnProperty(Continent.NAME, "Continent")
                    .groupingColumn()
                    .beanProperty("name"),
            columnProperty(Continent.SURFACE_AREA, "Surface area")
                    .aggregateColumn()
                    .numberFormatGrouping(true)
                    .beanProperty("surfaceArea"),
            columnProperty(Continent.POPULATION, "Population")
                    .aggregateColumn()
                    .numberFormatGrouping(true)
                    .beanProperty("population"),
            columnProperty(Continent.MIN_LIFE_EXPECTANCY, "Min. life expectancy")
                    .aggregateColumn()
                    .beanProperty("minLifeExpectancy"),
            columnProperty(Continent.MAX_LIFE_EXPECTANCY, "Max. life expectancy")
                    .aggregateColumn()
                    .beanProperty("maxLifeExpectancy"),
            columnProperty(Continent.MIN_INDEPENDENCE_YEAR, "Min. ind. year")
                    .aggregateColumn(),
            columnProperty(Continent.MAX_INDEPENDENCE_YEAR, "Max. ind. year")
                    .aggregateColumn(),
            columnProperty(Continent.GNP, "GNP")
                    .aggregateColumn()
                    .numberFormatGrouping(true)
                    .beanProperty("gnp"))
            .readOnly()
            .caption("Continent");
  }

  private static final class CityColorProvider implements ColorProvider {

    private static final long serialVersionUID = 1;

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

      return null;
    }
  }

  private static final class NoOfSpeakersProvider
          implements DerivedProperty.Provider<Integer> {

    private static final long serialVersionUID = 1;

    @Override
    public Integer get(final DerivedProperty.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;
    }
  }

  private static final class CityValidator
          extends DefaultEntityValidator implements Serializable {

    private static final long serialVersionUID = 1;

    @Override
    public void validate(Entity city, EntityDefinition cityDefinition) throws ValidationException {
      super.validate(city, cityDefinition);
      //after a call to super.validate() property values that are not nullable
      //(such as country and population) are guaranteed to be non-null
      Entity country = city.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");
      }
    }
  }

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

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

      return "POINT (" + geoPosition.getLatitude() + " " + geoPosition.getLongitude() + ")";
    }

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

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

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

2.3. Domain unit test

/*
 * Copyright (c) 2004 - 2021, 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.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 {
    getConnection().selectSingle(Lookup.CITY_NAME, "Genova");
  }

  @Override
  protected Entity initializeTestEntity(EntityType<?> entityType,
                                        Map<EntityType<?>, 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<EntityType<?>, Entity> foreignKeyEntities) {
    super.modifyEntity(testEntity, foreignKeyEntities);
    if (testEntity.getEntityType().equals(Country.TYPE)) {
      testEntity.put(Country.CONTINENT, "Europe");
    }
    else if (testEntity.getEntityType().equals(City.TYPE)) {
      testEntity.put(City.LOCATION, null);
    }
  }

  @Override
  protected Entity initializeReferenceEntity(EntityType<?> entityType,
                                             Map<EntityType<?>, Entity> foreignKeyEntities)
          throws DatabaseException{
    if (entityType.equals(Country.TYPE)) {
      return getEntities().builder(Country.TYPE)
              .with(Country.CODE, "ISL").build();
    }
    if (entityType.equals(City.TYPE)) {
      return getEntities().builder(City.TYPE)
              .with(City.ID, 1449)
              .build();
    }

    return super.initializeReferenceEntity(entityType, foreignKeyEntities);
  }
}

3. Model

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

import is.codion.common.db.exception.DatabaseException;
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.Country;
import is.codion.framework.domain.entity.Attribute;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.swing.common.ui.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.model.SwingEntityTableSortModel;

import org.json.JSONArray;
import org.json.JSONObject;
import org.jxmapviewer.viewer.GeoPosition;

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.List;

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

public final class CityTableModel extends SwingEntityTableModel {

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

  private final State locationUpdateCancelledState = State.state();

  public CityTableModel(final EntityConnectionProvider connectionProvider) {
    super(City.TYPE, connectionProvider, new CityTableSortModel(connectionProvider.getEntities()));
  }

  public List<Entity> updateLocationForSelected(ProgressReporter progressReporter)
          throws IOException, DatabaseException, ValidationException {
    List<Entity> updatedCities = new ArrayList<>();
    locationUpdateCancelledState.set(false);
    List<Entity> selectedCities = getSelectionModel().getSelectedItems();
    for (Entity city : selectedCities) {
      if (!locationUpdateCancelledState.get()) {
        progressReporter.setMessage(city.toString());
        updateLocation(city);
        updatedCities.add(city);
        progressReporter.setProgress(100 * updatedCities.size() / selectedCities.size());
      }
    }

    return updatedCities;
  }

  public void cancelLocationUpdate() {
    locationUpdateCancelledState.set(true);
  }

  public StateObserver getLocationUpdateCancelledObserver() {
    return locationUpdateCancelledState.getObserver();
  }

  private void updateLocation(Entity city) throws IOException, DatabaseException, ValidationException {
    JSONArray jsonArray = toJSONArray(new URL(OPENSTREETMAP_ORG_SEARCH +
            URLEncoder.encode(city.get(City.NAME), UTF_8.name()) + "," +
            URLEncoder.encode(city.getForeignKey(City.COUNTRY_FK).get(Country.NAME), UTF_8.name()) + "?format=json"));

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

  private void updateLocation(Entity city, JSONObject cityInformation) throws DatabaseException, ValidationException {
    city.put(City.LOCATION, new GeoPosition(cityInformation.getDouble("lat"), cityInformation.getDouble("lon")));
    getEditModel().update(singletonList(city)).get(0);
  }

  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 static final class CityTableSortModel extends SwingEntityTableSortModel {

    private CityTableSortModel(Entities entities) {
      super(entities);
    }

    @Override
    protected Comparable<?> getComparable(Entity entity, Attribute<?> attribute) {
      if (attribute.equals(City.LOCATION)) {
        return entity.getOptional(attribute)
                .map(Object::toString)
                .orElse(null);
      }

      return super.getComparable(entity, attribute);
    }
  }
}
package is.codion.framework.demos.world.model;

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

public final class CountryEditModel extends SwingEntityEditModel {

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

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

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

    return comboBoxModel;
  }

  public void setAverageCityPopulation(Double value) {
    averageCityPopulationValue.set(value);
  }

  public ValueObserver<Double> getAvarageCityPopulationValue() {
    return averageCityPopulationValue.getObserver();
  }
}
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.Country;
import is.codion.swing.framework.model.SwingEntityTableModel;

public class CountryTableModel extends SwingEntityTableModel {

  public CountryTableModel(EntityConnectionProvider connectionProvider) {
    super(Country.TYPE, connectionProvider);
    configureConditionModels();
  }

  private void configureConditionModels() {
    getTableConditionModel().getConditionModels().stream()
            .filter(model -> model.getColumnIdentifier().isString())
            .forEach(CountryTableModel::configureConditionModel);
  }

  private static void configureConditionModel(ColumnConditionModel<?, ?> model) {
    model.setCaseSensitive(false);
    model.setAutomaticWildcard(AutomaticWildcard.PREFIX_AND_POSTFIX);
  }
}
package is.codion.framework.demos.world.model;

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.CountryLanguage;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.model.SwingEntityTableModel;

import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.stream.Collectors;

public final class CountryModel extends SwingEntityModel {

  public CountryModel(EntityConnectionProvider connectionProvider) {
    super(new CountryEditModel(connectionProvider), new CountryTableModel(connectionProvider));
    SwingEntityModel cityModel = new SwingEntityModel(new CityTableModel(connectionProvider));
    SwingEntityModel countryLanguageModel = new SwingEntityModel(CountryLanguage.TYPE, connectionProvider);
    addDetailModels(cityModel, countryLanguageModel);
    bindEvents();
  }

  private void bindEvents() {
    getDetailModel(City.TYPE).getTableModel().addRefreshDoneListener(() ->
            ((CountryEditModel) getEditModel()).setAverageCityPopulation(getAverageCityPopulation()));
  }

  private Double getAverageCityPopulation() {
    SwingEntityTableModel cityTableModel = getDetailModel(City.TYPE).getTableModel();
    if (!getEditModel().isEntityNew()) {
      Entity country = getEditModel().getEntityCopy();

      List<City> cities = Entity.castTo(City.TYPE, cityTableModel.getItems()).stream()
              .filter(city -> city.isInCountry(country)).collect(Collectors.toList());

      OptionalDouble averageCityPopulation = cities.stream()
              .map(city -> city.getOptional(City.POPULATION))
              .filter(Optional::isPresent).map(Optional::get)
              .mapToInt(Integer::valueOf).average();

      return averageCityPopulation.isPresent() ? averageCityPopulation.getAsDouble() : null;
    }

    return null;
  }
}
package is.codion.framework.demos.world.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.domain.entity.Entity;

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

import java.util.List;

public final class CountryOverviewTableModel extends CountryTableModel {

  private final DefaultPieDataset citiesDataset = new DefaultPieDataset();
  private final DefaultPieDataset languagesDataset = new DefaultPieDataset();

  public CountryOverviewTableModel(final EntityConnectionProvider connectionProvider) {
    super(connectionProvider);
    bindEvents();
  }

  public PieDataset getCitiesDataset() {
    return citiesDataset;
  }

  public PieDataset getLanguagesDataset() {
    return languagesDataset;
  }

  private void bindEvents() {
    getSelectionModel().addSelectedItemsListener(this::refreshChartDatasets);
  }

  private void refreshChartDatasets(List<Entity> selectedCountries) {
    citiesDataset.clear();
    languagesDataset.clear();
    try {
      if (!selectedCountries.isEmpty()) {
        EntityConnection connection = getConnectionProvider().getConnection();

        List<City> cities = Entity.castTo(City.TYPE,
                connection.select(City.COUNTRY_FK, selectedCountries));

        cities.forEach(city -> citiesDataset.setValue(city.name(), city.population()));

        List<CountryLanguage> languages = Entity.castTo(CountryLanguage.TYPE,
                connection.select(CountryLanguage.COUNTRY_FK, selectedCountries));

        languages.forEach(language -> languagesDataset.setValue(language.language(), language.noOfSpeakers()));
      }
    }
    catch (DatabaseException e) {
      throw new RuntimeException(e);
    }
  }
}
package is.codion.framework.demos.world.model;

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.CountryLanguage;
import is.codion.swing.framework.model.SwingEntityModel;

public final class CountryOverviewModel extends SwingEntityModel {

  public CountryOverviewModel(EntityConnectionProvider connectionProvider) {
    super(new CountryEditModel(connectionProvider), new CountryOverviewTableModel(connectionProvider));
    SwingEntityModel cityModel = new SwingEntityModel(City.TYPE, connectionProvider);
    SwingEntityModel countryLanguageModel = new SwingEntityModel(CountryLanguage.TYPE, connectionProvider);
    addDetailModels(cityModel, countryLanguageModel);
  }
}
package is.codion.framework.demos.world.model;

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

public final class ContinentModel extends SwingEntityModel {

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

  public ContinentModel(EntityConnectionProvider connectionProvider) {
    super(Continent.TYPE, connectionProvider);
    getTableModel().addRefreshListener(this::refreshChartDatasets);
  }

  public PieDataset getPopulationDataset() {
    return populationDataset;
  }

  public PieDataset getSurfaceAreaDataset() {
    return surfaceAreaDataset;
  }

  public PieDataset getGnpDataset() {
    return gnpDataset;
  }

  public CategoryDataset getLifeExpectancyDataset() {
    return lifeExpectancyDataset;
  }

  private void refreshChartDatasets() {
    populationDataset.clear();
    surfaceAreaDataset.clear();
    gnpDataset.clear();
    lifeExpectancyDataset.clear();
    Entity.castTo(Continent.TYPE, getTableModel().getItems()).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());
    });
  }
}
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.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 {

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

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

  private void configureConditionModels() {
    getTableConditionModel().getConditionModels().stream()
            .filter(model -> model.getColumnIdentifier().isString())
            .forEach(LookupTableModel::configureConditionModel);
  }

  private static void configureConditionModel(ColumnConditionModel<?, ?> model) {
    model.setCaseSensitive(false);
    model.setAutomaticWildcard(AutomaticWildcard.PREFIX_AND_POSTFIX);
  }
}

3.1. Main application model

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

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 WorldAppModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider);
    setupEntityModels(connectionProvider);
  }

  private void setupEntityModels(EntityConnectionProvider connectionProvider) {
    SwingEntityModel countryModel = new CountryModel(connectionProvider);
    SwingEntityModel countryOverviewModel = new CountryOverviewModel(connectionProvider);
    SwingEntityModel lookupModel = new SwingEntityModel(new LookupTableModel(connectionProvider));
    SwingEntityModel continentModel = new ContinentModel(connectionProvider);

    addEntityModels(countryModel, countryOverviewModel, lookupModel, continentModel);
  }
}

4. UI

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.ComponentBuilders;
import is.codion.swing.common.ui.textfield.DoubleField;
import is.codion.swing.common.ui.value.ComponentValue;
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.JLabel;
import javax.swing.JPanel;

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

public final class CountryEditPanel extends EntityEditPanel {

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

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

    createTextField(Country.CODE)
            .upperCase(true);
    createTextField(Country.CODE_2)
            .upperCase(true);
    createTextField(Country.NAME);
    createItemComboBox(Country.CONTINENT)
            .preferredWidth(120);
    createAttributeComboBox(Country.REGION)
            .preferredWidth(120);
    createTextField(Country.SURFACEAREA);
    createTextField(Country.INDEPYEAR);
    createTextField(Country.POPULATION);
    createTextField(Country.LIFE_EXPECTANCY);
    createTextField(Country.GNP);
    createTextField(Country.GNPOLD);
    createTextField(Country.LOCALNAME);
    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)
                    .editPanelInitializer(this::initializeCapitalEditPanel)
                    .createEditPanelAction(capitalComboBox));
    //add a field displaying the avarage city population for the selected country
    ComponentValue<Double, DoubleField> averageCityPopulationFieldValue =
            ComponentBuilders.doubleField()
                    .maximumFractionDigits(2)
                    .focusable(false)
                    .editable(false)
                    .linkedValueObserver(((CountryEditModel) getEditModel()).getAvarageCityPopulationValue())
                    .buildComponentValue();

    setLayout(gridLayout(4, 5));

    addInputPanel(Country.CODE);
    addInputPanel(Country.CODE_2);
    addInputPanel(Country.NAME);
    addInputPanel(Country.CONTINENT);
    addInputPanel(Country.REGION);
    addInputPanel(Country.SURFACEAREA);
    addInputPanel(Country.INDEPYEAR);
    addInputPanel(Country.POPULATION);
    addInputPanel(Country.LIFE_EXPECTANCY);
    addInputPanel(Country.GNP);
    addInputPanel(Country.GNPOLD);
    addInputPanel(Country.LOCALNAME);
    addInputPanel(Country.GOVERNMENTFORM);
    addInputPanel(Country.HEADOFSTATE);
    addInputPanel(Country.CAPITAL_FK, capitalPanel);
    add(createInputPanel(new JLabel("Avg. city population"),
            averageCityPopulationFieldValue.getComponent()));
  }

  private void initializeCapitalEditPanel(EntityEditPanel capitalEditPanel) {
    //set the country to the one selected in the CountryEditPanel
    Entity country = getEditModel().getEntityCopy();
    if (country.getPrimaryKey().isNotNull()) {
      //if a country is selected, then we don't allow it to be changed
      capitalEditPanel.getEditModel().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.event.EventDataListener;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.model.ValueChange;
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 java.awt.BorderLayout;
import java.awt.Dimension;

import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static javax.swing.BorderFactory.createRaisedBevelBorder;

public final class CityEditPanel extends EntityEditPanel {

  public CityEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @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 = new JPanel(gridLayout(4, 1));
    inputPanel.add(createInputPanel(City.COUNTRY_FK));
    inputPanel.add(createInputPanel(City.NAME));
    inputPanel.add(createInputPanel(City.DISTRICT));
    inputPanel.add(createInputPanel(City.POPULATION));

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

    setLayout(borderLayout());
    add(inputBasePanel, BorderLayout.WEST);
    add(initializeMapKit(), BorderLayout.CENTER);
  }

  private JXMapKit initializeMapKit() {
    JXMapKit mapKit = new JXMapKit();
    mapKit.setPreferredSize(new Dimension(300, 300));
    mapKit.setTileFactory(new DefaultTileFactory(new OSMTileFactoryInfo()));
    mapKit.setMiniMapVisible(false);
    mapKit.setZoomSliderVisible(false);
    mapKit.setBorder(createRaisedBevelBorder());
    mapKit.getMainMap().setZoom(14);
    mapKit.getMainMap().setOverlayPainter(new WaypointPainter<>());

    getEditModel().addValueListener(City.LOCATION, new LocationListener(mapKit.getMainMap()));

    return mapKit;
  }

  private static final class LocationListener implements EventDataListener<ValueChange<GeoPosition>> {

    private final JXMapViewer mapViewer;

    private LocationListener(final JXMapViewer mapViewer) {
      this.mapViewer = mapViewer;
    }

    @Override
    public void onEvent(final ValueChange<GeoPosition> locationChange) {
      final WaypointPainter<Waypoint> overlayPainter = (WaypointPainter<Waypoint>) mapViewer.getOverlayPainter();
      if (locationChange.getValue() != null) {
        final GeoPosition position = locationChange.getValue();
        overlayPainter.setWaypoints(singleton((new DefaultWaypoint(position.getLatitude(), position.getLongitude()))));
        mapViewer.setCenterPosition(position);
      }
      else {
        overlayPainter.setWaypoints(emptySet());
        mapViewer.repaint();
      }
    }
  }
}
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 static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class CountryLanguageEditPanel extends EntityEditPanel {

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

    setLayout(gridLayout(2, 4));

    addInputPanel(CountryLanguage.COUNTRY_FK);
    addInputPanel(CountryLanguage.LANGUAGE);
    addInputPanel(CountryLanguage.IS_OFFICIAL);
    addInputPanel(CountryLanguage.PERCENTAGE);
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.common.model.table.ColumnSummary;
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.model.CountryOverviewTableModel;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.EntityTablePanel;

import org.jfree.chart.ChartPanel;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import java.awt.BorderLayout;
import java.awt.Dimension;

import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;
import static org.jfree.chart.ChartFactory.createPieChart;

public final class CountryOverviewPanel extends EntityPanel {

  private final ChartPanel cityChartPanel;
  private final ChartPanel languageChartPanel;

  public CountryOverviewPanel(SwingEntityModel entityModel) {
    super(entityModel, new CountryEditPanel(entityModel.getEditModel()));
    final CountryOverviewTableModel tableModel = (CountryOverviewTableModel) entityModel.getTableModel();
    cityChartPanel = new ChartPanel(createPieChart("Cities", tableModel.getCitiesDataset()));
    cityChartPanel.getChart().removeLegend();
    cityChartPanel.setPreferredSize(new Dimension(300, 300));
    languageChartPanel = new ChartPanel(createPieChart("Languages", tableModel.getLanguagesDataset()));
    languageChartPanel.getChart().removeLegend();
    languageChartPanel.setPreferredSize(new Dimension(300, 300));
  }

  @Override
  protected void initializeUI() {
    SwingEntityModel countryModel = getModel();
    countryModel.getTableModel().getColumnSummaryModel(Country.POPULATION).setSummary(ColumnSummary.SUM);
    SwingEntityModel cityModel = countryModel.getDetailModel(City.TYPE);
    cityModel.getTableModel().getColumnSummaryModel(City.POPULATION).setSummary(ColumnSummary.SUM);
    SwingEntityModel countryLanguageModel = countryModel.getDetailModel(CountryLanguage.TYPE);

    countryModel.addLinkedDetailModel(cityModel);
    countryModel.addLinkedDetailModel(countryLanguageModel);

    EntityTablePanel countryTablePanel = getTablePanel();
    countryTablePanel.getTable().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    countryTablePanel.getTable().setDoubleClickAction(Control.control(this::displayEditPanel));
    countryTablePanel.setSummaryPanelVisible(true);

    EntityPanel cityPanel = new EntityPanel(cityModel);
    cityPanel.setBorder(BorderFactory.createTitledBorder("Cities"));
    cityPanel.getTablePanel().getTable().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    cityPanel.getTablePanel().setSummaryPanelVisible(true);
    cityPanel.getTablePanel().setIncludeSouthPanel(false);

    EntityPanel languagePanel = new EntityPanel(countryLanguageModel);
    languagePanel.setBorder(BorderFactory.createTitledBorder("Languages"));
    languagePanel.getTablePanel().getTable().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    languagePanel.getTablePanel().setIncludeSouthPanel(false);

    EntityEditPanel countryEditPanel = getEditPanel();

    countryEditPanel.initializePanel();
    countryTablePanel.initializePanel();
    cityPanel.initializePanel();
    languagePanel.initializePanel();

    JPanel southTablePanel = new JPanel(gridLayout(1, 2));
    southTablePanel.add(cityPanel);
    southTablePanel.add(languagePanel);

    JPanel southChartPanel = new JPanel(gridLayout(1, 2));
    southChartPanel.add(cityChartPanel);
    southChartPanel.add(languageChartPanel);

    JTabbedPane southTabbedPane = new JTabbedPane(SwingConstants.BOTTOM);
    southTabbedPane.addTab("Tables", southTablePanel);
    southTabbedPane.addTab("Charts", southChartPanel);
    southTabbedPane.setMnemonicAt(0, 'T');
    southTabbedPane.setMnemonicAt(1, 'C');

    setLayout(borderLayout());

    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, countryTablePanel, southTabbedPane);
    splitPane.setContinuousLayout(true);
    splitPane.setResizeWeight(0.33);
    add(splitPane, BorderLayout.CENTER);

    initializeEditControlPanel();
    initializeKeyboardActions();
    initializeNavigation();
  }

  private void displayEditPanel() {
    final JPanel editPanel = getEditControlPanel();
    if (!editPanel.isShowing()) {
      Dialogs.componentDialogBuilder(editPanel)
              .owner(this)
              .modal(false)
              .show();
    }
    getEditPanel().requestInitialFocus();
  }
}
package is.codion.framework.demos.world.ui;

import is.codion.framework.demos.world.model.ContinentModel;
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 java.awt.BorderLayout;
import java.awt.Dimension;

import static is.codion.swing.common.ui.Components.setPreferredHeight;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;
import static org.jfree.chart.ChartFactory.createBarChart;
import static org.jfree.chart.ChartFactory.createPieChart;

public final class ContinentPanel extends EntityPanel {

  public ContinentPanel(SwingEntityModel entityModel) {
    super(entityModel, new ContinentTablePanel(entityModel.getTableModel()));
  }

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

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

    ChartPanel populationChartPanel = new ChartPanel(createPieChart("Population",
            model.getPopulationDataset()));
    populationChartPanel.setPreferredSize(new Dimension(300, 300));
    ChartPanel surfaceAreaChartPanel = new ChartPanel(createPieChart("Surface area",
            model.getSurfaceAreaDataset()));
    surfaceAreaChartPanel.setPreferredSize(new Dimension(300, 300));
    ChartPanel gnpChartPanel = new ChartPanel(createPieChart("GNP",
            model.getGnpDataset()));
    gnpChartPanel.setPreferredSize(new Dimension(300, 300));
    ChartPanel lifeExpectancyChartPanel = new ChartPanel(createBarChart("Life expectancy",
            "Continent", "Years",
            model.getLifeExpectancyDataset()));

    JPanel centerPanel = new JPanel(borderLayout());
    centerPanel.add(tablePanel, BorderLayout.NORTH);
    centerPanel.add(lifeExpectancyChartPanel, BorderLayout.CENTER);

    JPanel southChartPanel = new JPanel(gridLayout(1, 3));
    southChartPanel.add(populationChartPanel);
    southChartPanel.add(surfaceAreaChartPanel);
    southChartPanel.add(gnpChartPanel);

    setLayout(borderLayout());

    add(centerPanel, BorderLayout.CENTER);
    add(southChartPanel, BorderLayout.SOUTH);

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

public final class ContinentTablePanel extends EntityTablePanel {

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

  @Override
  protected Controls getPopupControls(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.common.ui.worker.ProgressWorker;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;

import org.kordamp.ikonli.foundation.Foundation;
import org.kordamp.ikonli.swing.FontIcon;

import java.io.File;
import java.io.IOException;
import java.util.List;

import static is.codion.swing.plugin.ikonli.foundation.IkonliFoundationIcons.ICON_SIZE;
import static is.codion.swing.plugin.ikonli.foundation.IkonliFoundationIcons.imageIcon;

public final class LookupTablePanel extends EntityTablePanel {

  public LookupTablePanel(SwingEntityTableModel tableModel) {
    super(tableModel);
    setConditionPanelVisible(true);
  }

  @Override
  protected Controls getPopupControls(List<Controls> additionalPopupControls) {
    return super.getPopupControls(additionalPopupControls)
            .addSeparatorAt(1)
            .addAt(2, Control.builder(this::exportCSV)
                    .caption("Export CSV...")
                    .icon(imageIcon(FontIcon.of(Foundation.PAGE_EXPORT_CSV, ICON_SIZE)))
                    .build());
  }

  private void exportCSV() throws IOException {
    File fileToSave = Dialogs.fileSelectionDialogBuilder()
            .owner(this)
            .selectFileToSave("export.csv");
    ProgressWorker.builder(() -> ((LookupTableModel) getTableModel()).exportCSV(fileToSave))
            .owner(this)
            .title("Exporting data")
            .successMessage("Export successful")
            .failTitle("Export failed")
            .execute();
  }
}

4.1. Main application panel

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

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
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.Continent;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
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.CountryOverviewModel;
import is.codion.framework.demos.world.model.WorldAppModel;
import is.codion.swing.common.ui.icons.Icons;
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.icons.FrameworkIcons;
import is.codion.swing.plugin.ikonli.foundation.IkonliFoundationFrameworkIcons;
import is.codion.swing.plugin.ikonli.foundation.IkonliFoundationIcons;

import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import java.awt.Color;
import java.awt.Dimension;
import java.util.List;
import java.util.Locale;

import static java.util.Arrays.asList;

public final class WorldAppPanel extends EntityApplicationPanel<WorldAppModel> {

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

  @Override
  protected List<EntityPanel> initializeEntityPanels(final WorldAppModel applicationModel) {
    SwingEntityModel countryModel = applicationModel.getEntityModel(CountryModel.class);
    SwingEntityModel countryOverviewModel = applicationModel.getEntityModel(CountryOverviewModel.class);
    SwingEntityModel cityModel = countryModel.getDetailModel(City.TYPE);
    SwingEntityModel countryLanguageModel = countryModel.getDetailModel(CountryLanguage.TYPE);
    SwingEntityModel continentModel = applicationModel.getEntityModel(Continent.TYPE);
    SwingEntityModel lookupModel = applicationModel.getEntityModel(Lookup.TYPE);

    EntityPanel countryPanel = new EntityPanel(countryModel,
            new CountryEditPanel(countryModel.getEditModel()));
    countryPanel.setDetailSplitPanelResizeWeight(0.7);
    countryModel.refresh();

    EntityPanel countryOverviewPanel = new CountryOverviewPanel(countryOverviewModel);
    countryOverviewPanel.setCaption("Country Overview");
    countryOverviewModel.refresh();

    EntityPanel cityPanel = new EntityPanel(cityModel,
            new CityEditPanel(cityModel.getEditModel()),
            new CityTablePanel(cityModel.getTableModel()));

    EntityPanel countryLanguagePanel = new EntityPanel(countryLanguageModel,
            new CountryLanguageEditPanel(countryLanguageModel.getEditModel()));

    countryPanel.addDetailPanels(cityPanel, countryLanguagePanel);

    EntityPanel continentPanel = new ContinentPanel(continentModel);
    continentModel.refresh();

    EntityPanel lookupPanel = new EntityPanel(lookupModel, new LookupTablePanel(lookupModel.getTableModel()));

    return asList(countryPanel, countryOverviewPanel, continentPanel, lookupPanel);
  }

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

  public static void main(final String[] args) throws CancelException {
    Locale.setDefault(new Locale("en", "EN"));
    UIManager.put("Table.alternateRowColor", new Color(215, 215, 215));
    Icons.ICONS_CLASSNAME.set(IkonliFoundationIcons.class.getName());
    FrameworkIcons.FRAMEWORK_ICONS_CLASSNAME.set(IkonliFoundationFrameworkIcons.class.getName());
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING.set(ReferentialIntegrityErrorHandling.DEPENDENCIES);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.world.domain.WorldImpl");
    SwingUtilities.invokeLater(() -> new WorldAppPanel().starter()
            .frameSize(new Dimension(1024, 720))
            .defaultLoginUser(User.parseUser("scott:tiger"))
            .start());
  }
}