This tutorial assumes you have at least skimmed the Domain model part of the Codion manual.
Domain model
API
public interface World {
DomainType DOMAIN = DomainType.domainType(World.class);
The domain API sections below continue the World class.
Implementation
public final class WorldImpl extends DomainModel {
private static final List<Item<String>> CONTINENT_ITEMS = List.of(
item("Africa"), item("Antarctica"), item("Asia"),
item("Europe"), item("North America"), item("Oceania"),
item("South America"));
public WorldImpl() {
super(DOMAIN);
//By default, you can't define a foreign key referencing an entity which
//has not been defined, to prevent mistakes. But sometimes we have to
//deal with cyclical dependencies, such as here, where city references
//country and country references city. In these cases we can simply
//disable foreign key validation.
validateForeignKeys(false);
add(city(), country(), countryLanguage(), lookup(), continent());
add(Country.AVERAGE_CITY_POPULATION, new AverageCityPopulationFunction());
}
The domain implementation sections below continue the WorldImpl class.
Countries
Country
SQL
create table world.country (
code varchar(3) not null,
name varchar(52) not null,
continent varchar(20) not null,
region varchar(26) not null,
surfacearea decimal(10,2) not null,
indepyear smallint,
population int not null,
lifeexpectancy decimal(3,1),
gnp decimal(10,2),
gnpold decimal(10,2),
localname varchar(45) not null,
governmentform varchar(45) not null,
headofstate varchar(60),
capital int,
code2 varchar(2) not null,
flag blob,
constraint country_pk primary key (code),
constraint continent_chk check(continent in ('Asia','Europe','North America','Africa','Oceania','Antarctica','South America'))
);
Domain
API
interface Country {
EntityType TYPE = DOMAIN.entityType("world.country");
Column<String> CODE = TYPE.stringColumn("code");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> CONTINENT = TYPE.stringColumn("continent");
Column<String> REGION = TYPE.stringColumn("region");
Column<Double> SURFACEAREA = TYPE.doubleColumn("surfacearea");
Column<Integer> INDEPYEAR = TYPE.integerColumn("indepyear");
Column<String> INDEPYEAR_SEARCHABLE = TYPE.stringColumn("indepyear_searchable");
Column<Integer> POPULATION = TYPE.integerColumn("population");
Column<Double> LIFE_EXPECTANCY = TYPE.doubleColumn("lifeexpectancy");
Column<Double> GNP = TYPE.doubleColumn("gnp");
Column<Double> GNPOLD = TYPE.doubleColumn("gnpold");
Column<String> LOCALNAME = TYPE.stringColumn("localname");
Column<String> GOVERNMENTFORM = TYPE.stringColumn("governmentform");
Column<String> HEADOFSTATE = TYPE.stringColumn("headofstate");
Column<Integer> CAPITAL = TYPE.integerColumn("capital");
Column<String> CODE_2 = TYPE.stringColumn("code2");
Attribute<Integer> CAPITAL_POPULATION = TYPE.integerAttribute("capital_population");
Column<Integer> NO_OF_CITIES = TYPE.integerColumn("no_of_cities");
Column<Integer> NO_OF_LANGUAGES = TYPE.integerColumn("no_of_languages");
Column<byte[]> FLAG = TYPE.byteArrayColumn("flag");
ForeignKey CAPITAL_FK = TYPE.foreignKey("capital_fk", CAPITAL, City.ID);
FunctionType<EntityConnection, String, Double> AVERAGE_CITY_POPULATION = functionType("average_city_population");
}
Implementation
EntityDefinition country() {
return Country.TYPE.define(
Country.CODE.define()
.primaryKey()
.caption("Country code")
.updatable(true)
.maximumLength(3),
Country.NAME.define()
.column()
.caption("Name")
.searchable(true)
.nullable(false)
.maximumLength(52),
Country.CONTINENT.define()
.column()
.caption("Continent")
.items(CONTINENT_ITEMS)
.nullable(false),
Country.REGION.define()
.column()
.caption("Region")
.nullable(false)
.maximumLength(26),
Country.SURFACEAREA.define()
.column()
.caption("Surface area")
.nullable(false)
.numberFormatGrouping(true)
.maximumFractionDigits(2),
Country.INDEPYEAR.define()
.column()
.caption("Indep. year")
.valueRange(-2000, 2500),
Country.INDEPYEAR_SEARCHABLE.define()
.column()
.expression("to_char(indepyear)")
.searchable(true)
.readOnly(true),
Country.POPULATION.define()
.column()
.caption("Population")
.nullable(false)
.numberFormatGrouping(true),
Country.LIFE_EXPECTANCY.define()
.column()
.caption("Life expectancy")
.maximumFractionDigits(1)
.valueRange(0, 99),
Country.GNP.define()
.column()
.caption("GNP")
.numberFormatGrouping(true)
.maximumFractionDigits(2),
Country.GNPOLD.define()
.column()
.caption("GNP old")
.numberFormatGrouping(true)
.maximumFractionDigits(2),
Country.LOCALNAME.define()
.column()
.caption("Local name")
.nullable(false)
.maximumLength(45),
Country.GOVERNMENTFORM.define()
.column()
.caption("Government form")
.nullable(false),
Country.HEADOFSTATE.define()
.column()
.caption("Head of state")
.maximumLength(60),
Country.CAPITAL.define()
.column(),
Country.CAPITAL_FK.define()
.foreignKey()
.caption("Capital"),
Country.CAPITAL_POPULATION.define()
.denormalized(Country.CAPITAL_FK, City.POPULATION)
.caption("Capital pop.")
.numberFormatGrouping(true),
Country.NO_OF_CITIES.define()
.subquery("""
SELECT COUNT(*)
FROM world.city
WHERE city.countrycode = country.code""")
.caption("No. of cities"),
Country.NO_OF_LANGUAGES.define()
.subquery("""
SELECT COUNT(*)
FROM world.countrylanguage
WHERE countrylanguage.countrycode = country.code""")
.caption("No. of languages"),
Country.FLAG.define()
.column()
.caption("Flag")
.lazy(true),
Country.CODE_2.define()
.column()
.caption("Code2")
.nullable(false)
.maximumLength(2))
.orderBy(ascending(Country.NAME))
.stringFactory(Country.NAME)
.caption("Country")
.build();
}
private static final class AverageCityPopulationFunction implements DatabaseFunction<EntityConnection, String, Double> {
@Override
public Double execute(EntityConnection connection, String countryCode) throws DatabaseException {
return connection.select(where(City.COUNTRY_CODE.equalTo(countryCode))
.attributes(City.POPULATION)
.build()).stream()
.map(city -> city.get(City.POPULATION))
.mapToInt(Integer::intValue)
.average()
.orElse(0d);
}
}
Model
CountryModel
package is.codion.framework.demos.world.model;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityModel;
public final class CountryModel extends SwingEntityModel {
CountryModel(EntityConnectionProvider connectionProvider) {
super(new CountryTableModel(connectionProvider));
SwingEntityModel cityModel = new SwingEntityModel(new CityTableModel(connectionProvider));
SwingEntityModel countryLanguageModel = new SwingEntityModel(new CountryLanguageTableModel(connectionProvider));
addDetailModels(cityModel, countryLanguageModel);
}
}
CountryEditModel
package is.codion.framework.demos.world.model;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.value.Value;
import is.codion.common.value.ValueObserver;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.component.EntityComboBoxModel;
import java.util.Objects;
public final class CountryEditModel extends SwingEntityEditModel {
private final Value<Double> averageCityPopulation = Value.value();
CountryEditModel(EntityConnectionProvider connectionProvider) {
super(Country.TYPE, connectionProvider);
initializeComboBoxModels(Country.CAPITAL_FK);
entity().addConsumer(country ->
averageCityPopulation.set(averageCityPopulation(country)));
}
@Override
public EntityComboBoxModel createForeignKeyComboBoxModel(ForeignKey foreignKey) {
EntityComboBoxModel comboBoxModel = super.createForeignKeyComboBoxModel(foreignKey);
if (foreignKey.equals(Country.CAPITAL_FK)) {
//only show cities for currently selected country
entity().addConsumer(country ->
comboBoxModel.items().visible().predicate().set(city ->
country != null && Objects.equals(city.get(City.COUNTRY_FK), country)));
}
return comboBoxModel;
}
public ValueObserver<Double> averageCityPopulation() {
return averageCityPopulation.observer();
}
private Double averageCityPopulation(Entity country) {
if (country == null) {
return null;
}
try {
return connection().execute(Country.AVERAGE_CITY_POPULATION, country.get(Country.CODE));
}
catch (DatabaseException e) {
throw new RuntimeException(e);
}
}
}
CountryTableModel
package is.codion.framework.demos.world.model;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.report.ReportException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.condition.Condition;
import is.codion.framework.model.ForeignKeyConditionModel;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.framework.model.SwingEntityTableModel;
import net.sf.jasperreports.engine.JasperPrint;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import static is.codion.plugin.jasperreports.JasperReports.classPathReport;
import static is.codion.plugin.jasperreports.JasperReports.fillReport;
import static java.util.Collections.singletonMap;
public final class CountryTableModel extends SwingEntityTableModel {
private static final String CITY_SUBREPORT_PARAMETER = "CITY_SUBREPORT";
private static final String COUNTRY_REPORT = "country_report.jasper";
private static final String CITY_REPORT = "city_report.jasper";
CountryTableModel(EntityConnectionProvider connectionProvider) {
super(new CountryEditModel(connectionProvider));
configureCapitalConditionModel();
}
public JasperPrint fillCountryReport(ProgressReporter<String> progressReporter) throws ReportException {
CountryReportDataSource dataSource =
new CountryReportDataSource(selection().items().get().iterator(),
connection(), progressReporter);
return fillReport(classPathReport(CountryTableModel.class, COUNTRY_REPORT), dataSource, reportParameters());
}
private static Map<String, Object> reportParameters() throws ReportException {
return new HashMap<>(singletonMap(CITY_SUBREPORT_PARAMETER,
classPathReport(CityTableModel.class, CITY_REPORT).load()));
}
private void configureCapitalConditionModel() {
ForeignKeyConditionModel capitalConditionModel =
(ForeignKeyConditionModel) queryModel().conditions()
.attribute(Country.CAPITAL_FK);
CapitalConditionSupplier capitalCondition = new CapitalConditionSupplier();
capitalConditionModel.equalSearchModel().condition().set(capitalCondition);
capitalConditionModel.inSearchModel().condition().set(capitalCondition);
}
private final class CapitalConditionSupplier implements Supplier<Condition> {
@Override
public Condition get() {
EntityConnection connection = connection();
try {
return City.ID.in(connection.select(Country.CAPITAL));
}
catch (DatabaseException e) {
throw new RuntimeException(e);
}
}
}
}
/*
* This file is part of Codion World Demo.
*
* Codion World Demo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Codion World Demo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Codion World Demo. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (c) 2024, Björn Darri Sigurðsson.
*/
package is.codion.framework.demos.world.model;
import is.codion.common.db.report.ReportException;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.demos.world.domain.WorldImpl;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperPrint;
import org.junit.jupiter.api.Test;
public final class CountryTableModelTest {
private static final User UNIT_TEST_USER =
User.parse(System.getProperty("codion.test.user", "scott:tiger"));
@Test
void fillCountryReport() throws ReportException, JRException {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
CountryTableModel tableModel = new CountryTableModel(connectionProvider);
tableModel.queryModel().conditions().get(Country.CODE).operands().equal().set("ISL");
tableModel.refresh();
tableModel.selection().index().set(0);
JasperPrint jasperPrint = tableModel.fillCountryReport(new ProgressReporter<String>() {
@Override
public void report(int progress) {}
@Override
public void publish(String... chunks) {}
});
JasperExportManager.getInstance(DefaultJasperReportsContext.getInstance()).exportToPdf(jasperPrint);
}
}
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
}
country_report.jrxml
<jasperReport name="country_report" language="java" pageWidth="595" pageHeight="842" columnWidth="535" leftMargin="30" rightMargin="30" topMargin="20" bottomMargin="20" uuid="65647229-b9ea-492c-9579-df6c5298a9a2">
<property name="ireport.scriptlethandling" value="2"/>
<property name="ireport.encoding" value="UTF-8"/>
<property name="ireport.zoom" value="1.0"/>
<property name="ireport.x" value="0"/>
<property name="ireport.y" value="0"/>
<import>is.codion.framework.demos.world.model.CountryReportDataSource</import>
<parameter name="CITY_SUBREPORT" class="net.sf.jasperreports.engine.JasperReport"/>
<field name="name" class="java.lang.String"/>
<field name="continent" class="java.lang.String"/>
<field name="region" class="java.lang.String"/>
<field name="surfacearea" class="java.lang.Double"/>
<field name="population" class="java.lang.Integer"/>
<background splitType="Stretch"/>
<detail>
<band height="52">
<element kind="textField" uuid="52591026-2ffa-4ca3-b73d-31bb53463220" x="0" y="14" width="100" height="14">
<expression><![CDATA[$F{name}]]></expression>
</element>
<element kind="textField" uuid="a37319e9-d3d7-4adc-a058-4783281221be" x="100" y="14" width="100" height="14">
<expression><![CDATA[$F{continent}]]></expression>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<element kind="textField" uuid="94bb3563-d458-47f3-bb98-bb9914e458f7" x="200" y="14" width="140" height="14">
<expression><![CDATA[$F{region}]]></expression>
</element>
<element kind="textField" uuid="7ae9640c-d57f-4cd9-b08c-b2e582ae4084" x="350" y="14" width="84" height="14" pattern="###,###,###,###" hTextAlign="Center">
<expression><![CDATA[$F{surfacearea}]]></expression>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<element kind="textField" uuid="0aeaac02-df9f-4c84-9533-3daee90d0082" x="435" y="14" width="100" height="14" pattern="###,###,###,###" hTextAlign="Center">
<expression><![CDATA[$F{population}]]></expression>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<element kind="subreport" uuid="fe0b73e2-e164-4dd6-a08d-9afb8d964fc2" x="-30" y="30" width="595" height="16">
<dataSourceExpression><![CDATA[((CountryReportDataSource) $P{REPORT_DATA_SOURCE}).cityDataSource()]]></dataSourceExpression>
<expression><![CDATA[$P{CITY_SUBREPORT}]]></expression>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<element kind="staticText" uuid="c61d049d-040d-4aba-9cf4-4aa277459142" x="0" y="0" width="100" height="14" bold="true">
<text><![CDATA[Name]]></text>
</element>
<element kind="staticText" uuid="2fcd930a-a99f-4f90-b303-4b33938c1a34" x="100" y="0" width="100" height="14" bold="true">
<text><![CDATA[Continent]]></text>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<element kind="staticText" uuid="510dab14-feac-4935-b8b6-130686c249dc" x="200" y="0" width="140" height="14" bold="true">
<text><![CDATA[Region]]></text>
</element>
<element kind="staticText" uuid="2ee9691c-9b08-44ea-a57f-fc3e58608a95" x="350" y="0" width="84" height="14" bold="true" hTextAlign="Center">
<text><![CDATA[Surface area]]></text>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<element kind="staticText" uuid="ac3794a2-c395-49ea-848f-066599941669" x="435" y="0" width="100" height="14" bold="true" hTextAlign="Center">
<text><![CDATA[Population]]></text>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</element>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</band>
</detail>
</jasperReport>
city_report.jrxml
<jasperReport name="city_report" language="java" pageWidth="595" pageHeight="842" columnWidth="535" leftMargin="30" rightMargin="30" topMargin="0" bottomMargin="0" uuid="65647229-b9ea-492c-9579-df6c5298a9a2">
<property name="ireport.scriptlethandling" value="2"/>
<property name="ireport.encoding" value="UTF-8"/>
<property name="ireport.zoom" value="1.0"/>
<property name="ireport.x" value="0"/>
<property name="ireport.y" value="0"/>
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
<field name="name" class="java.lang.String"/>
<field name="population" class="java.lang.Integer"/>
<background splitType="Stretch"/>
<columnHeader height="16">
<element kind="staticText" uuid="ef712d38-e1c6-4922-8e38-4bd263230db5" x="350" y="2" width="84" height="14" bold="true">
<text><![CDATA[Name]]></text>
</element>
<element kind="staticText" uuid="8c05be12-bf23-4fac-ac18-47a1e18f5959" x="434" y="2" width="100" height="14" bold="true" hTextAlign="Center">
<text><![CDATA[Population]]></text>
</element>
<element kind="staticText" uuid="44915dc4-f2b8-4661-9b9a-8cb6e6f91f58" x="250" y="2" width="100" height="14" bold="true">
<text><![CDATA[Largest Cities]]></text>
</element>
<element kind="line" uuid="468587c6-550e-4dc1-80c2-72074816c6ff" x="0" y="1" width="535" height="1">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<pen lineStyle="Dotted"/>
</element>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</columnHeader>
<detail>
<band height="14">
<element kind="textField" uuid="52591026-2ffa-4ca3-b73d-31bb53463220" x="350" y="0" width="84" height="14">
<expression><![CDATA[$F{name}]]></expression>
</element>
<element kind="textField" uuid="376a08f3-5e56-45cd-974e-3635b8248373" x="434" y="0" width="100" height="14" pattern="###,###,###,###" hTextAlign="Center">
<expression><![CDATA[$F{population}]]></expression>
</element>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</band>
</detail>
<columnFooter height="1">
<element kind="line" uuid="e0b89b1f-85c9-4dd3-a316-a4a8359db499" x="0" y="0" width="535" height="1">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<pen lineStyle="Dotted"/>
</element>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</columnFooter>
</jasperReport>
CountryReportDataSource
package is.codion.framework.demos.world.model;
import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.plugin.jasperreports.JasperReportsDataSource;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRField;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import static is.codion.framework.db.EntityConnection.Select.where;
import static is.codion.framework.domain.entity.OrderBy.descending;
public final class CountryReportDataSource extends JasperReportsDataSource<Entity> {
private final EntityConnection connection;
CountryReportDataSource(Iterator<Entity> countryIterator, EntityConnection connection,
ProgressReporter<String> progressReporter) {
super(countryIterator, new CountryValueProvider(),
new CountryReportProgressReporter(progressReporter));
this.connection = connection;
}
/* See usage in src/main/reports/country_report.jrxml, subreport element */
public JRDataSource cityDataSource() {
Entity country = currentItem();
try {
Collection<Entity> largestCities =
connection.select(where(City.COUNTRY_FK.equalTo(country))
.attributes(City.NAME, City.POPULATION)
.orderBy(descending(City.POPULATION))
.limit(5)
.build());
return new JasperReportsDataSource<>(largestCities.iterator(), new CityValueProvider());
}
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 country, JRField field) {
return switch (field.getName()) {
case NAME -> country.get(Country.NAME);
case CONTINENT -> country.get(Country.CONTINENT);
case REGION -> country.get(Country.REGION);
case SURFACEAREA -> country.get(Country.SURFACEAREA);
case POPULATION -> country.get(Country.POPULATION);
default -> throw new IllegalArgumentException("Unknown field: " + field.getName());
};
}
}
private static final class CityValueProvider implements BiFunction<Entity, JRField, Object> {
private static final String NAME = "name";
private static final String POPULATION = "population";
@Override
public Object apply(Entity city, JRField field) {
return switch (field.getName()) {
case NAME -> city.get(City.NAME);
case POPULATION -> city.get(City.POPULATION);
default -> throw new IllegalArgumentException("Unknown field: " + field.getName());
};
}
}
private static final class CountryReportProgressReporter implements Consumer<Entity> {
private final AtomicInteger counter = new AtomicInteger();
private final ProgressReporter<String> progressReporter;
private CountryReportProgressReporter(ProgressReporter<String> progressReporter) {
this.progressReporter = progressReporter;
}
@Override
public void accept(Entity country) {
progressReporter.publish(country.get(Country.NAME));
progressReporter.report(counter.incrementAndGet());
}
}
}
CountryReportDataSourceTest
package is.codion.framework.demos.world.model;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.user.User;
import is.codion.common.value.Value;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.demos.world.domain.WorldImpl;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.base.JRBaseField;
import org.junit.jupiter.api.Test;
import java.util.List;
import static is.codion.framework.db.EntityConnection.Select.where;
import static is.codion.framework.domain.entity.OrderBy.ascending;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public final class CountryReportDataSourceTest {
private static final User UNIT_TEST_USER =
User.parse(System.getProperty("codion.test.user", "scott:tiger"));
@Test
void iterate() throws DatabaseException, JRException {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
Value<Integer> progressCounter = Value.value();
Value<String> publishedValue = Value.value();
ProgressReporter<String> progressReporter = new ProgressReporter<>() {
@Override
public void report(int progress) {
progressCounter.set(progress);
}
@Override
public void publish(String... chunks) {
publishedValue.set(chunks[0]);
}
};
EntityConnection connection = connectionProvider.connection();
List<Entity> countries =
connection.select(where(Country.NAME.in("Denmark", "Iceland"))
.orderBy(ascending(Country.NAME))
.build());
CountryReportDataSource countryReportDataSource = new CountryReportDataSource(countries.iterator(), connection, progressReporter);
assertThrows(IllegalStateException.class, countryReportDataSource::cityDataSource);
countryReportDataSource.next();
assertEquals("Denmark", countryReportDataSource.getFieldValue(field(Country.NAME)));
assertEquals("Europe", countryReportDataSource.getFieldValue(field(Country.CONTINENT)));
assertEquals("Nordic Countries", countryReportDataSource.getFieldValue(field(Country.REGION)));
assertEquals(43094d, countryReportDataSource.getFieldValue(field(Country.SURFACEAREA)));
assertEquals(5330000, countryReportDataSource.getFieldValue(field(Country.POPULATION)));
assertThrows(JRException.class, () -> countryReportDataSource.getFieldValue(field(City.LOCATION)));
JRDataSource denmarkCityDataSource = countryReportDataSource.cityDataSource();
denmarkCityDataSource.next();
assertEquals("K\u00F8benhavn", denmarkCityDataSource.getFieldValue(field(City.NAME)));
assertEquals(495699, denmarkCityDataSource.getFieldValue(field(City.POPULATION)));
assertThrows(JRException.class, () -> denmarkCityDataSource.getFieldValue(field(Country.REGION)));
denmarkCityDataSource.next();
assertEquals("\u00C5rhus", denmarkCityDataSource.getFieldValue(field(City.NAME)));
countryReportDataSource.next();
assertEquals("Iceland", countryReportDataSource.getFieldValue(field(Country.NAME)));
JRDataSource icelandCityDataSource = countryReportDataSource.cityDataSource();
icelandCityDataSource.next();
assertEquals("Reykjav\u00EDk", icelandCityDataSource.getFieldValue(field(City.NAME)));
assertEquals(2, progressCounter.get());
assertEquals("Iceland", publishedValue.get());
}
}
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
private static JRField field(Attribute<?> attribute) {
return new TestField(attribute.name());
}
private static final class TestField extends JRBaseField {
private TestField(String name) {
this.name = name;
}
}
}
UI
CountryPanel
package is.codion.framework.demos.world.ui;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.demos.world.model.CountryModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.TabbedDetailLayout;
final class CountryPanel extends EntityPanel {
CountryPanel(CountryModel countryModel) {
super(countryModel,
new CountryEditPanel(countryModel.editModel()),
new CountryTablePanel(countryModel.tableModel()),
config -> config.detailLayout(entityPanel -> TabbedDetailLayout.builder(entityPanel)
.splitPaneResizeWeight(0.7)
.build()));
SwingEntityModel cityModel = countryModel.detailModel(City.TYPE);
EntityPanel cityPanel = new EntityPanel(cityModel,
new CityEditPanel(cityModel.tableModel()),
new CityTablePanel(cityModel.tableModel()));
SwingEntityModel countryLanguageModel = countryModel.detailModel(CountryLanguage.TYPE);
EntityPanel countryLanguagePanel = new EntityPanel(countryLanguageModel,
new CountryLanguageEditPanel(countryLanguageModel.editModel()),
new CountryLanguageTablePanel(countryLanguageModel.tableModel()));
addDetailPanels(cityPanel, countryLanguagePanel);
}
}
CountryEditPanel
package is.codion.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.swing.common.ui.component.text.NumberField;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import static is.codion.swing.common.ui.component.Components.*;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;
final class CountryEditPanel extends EntityEditPanel {
private static final int PREFERRED_COMBO_BOX_WIDTH = 120;
CountryEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(Country.CODE);
createTextField(Country.CODE)
.columns(6)
.upperCase(true);
createTextField(Country.CODE_2)
.columns(6)
.upperCase(true);
createTextField(Country.NAME);
createTextField(Country.LOCALNAME);
createItemComboBox(Country.CONTINENT)
.preferredWidth(PREFERRED_COMBO_BOX_WIDTH);
createComboBox(Country.REGION)
.preferredWidth(PREFERRED_COMBO_BOX_WIDTH);
createTextField(Country.SURFACEAREA)
.columns(5);
createTextField(Country.INDEPYEAR)
.columns(5);
createTextField(Country.POPULATION)
.columns(5);
createTextField(Country.LIFE_EXPECTANCY)
.columns(5);
createTextField(Country.GNP)
.columns(6);
createTextField(Country.GNPOLD)
.columns(6);
createComboBox(Country.GOVERNMENTFORM)
.preferredWidth(PREFERRED_COMBO_BOX_WIDTH)
.editable(true);
createTextField(Country.HEADOFSTATE);
//create a panel with a button for adding a new city
createForeignKeyComboBoxPanel(Country.CAPITAL_FK, this::createCapitalEditPanel)
.includeAddButton(true);
//add a field displaying the avarage city population for the selected country
CountryEditModel editModel = editModel();
NumberField<Double> averageCityPopulationField = doubleField()
.link(editModel.averageCityPopulation())
.maximumFractionDigits(2)
.groupingUsed(true)
.horizontalAlignment(SwingConstants.CENTER)
.focusable(false)
.editable(false)
.build();
JPanel codePanel = gridLayoutPanel(1, 2)
.add(createInputPanel(Country.CODE))
.add(createInputPanel(Country.CODE_2))
.build();
JPanel gnpPanel = gridLayoutPanel(1, 2)
.add(createInputPanel(Country.GNP))
.add(createInputPanel(Country.GNPOLD))
.build();
JPanel surfaceAreaIndYearPanel = gridLayoutPanel(1, 2)
.add(createInputPanel(Country.SURFACEAREA))
.add(createInputPanel(Country.INDEPYEAR))
.build();
JPanel populationLifeExpectancyPanel = gridLayoutPanel(1, 2)
.add(createInputPanel(Country.POPULATION))
.add(createInputPanel(Country.LIFE_EXPECTANCY))
.build();
setLayout(gridLayout(4, 5));
add(codePanel);
addInputPanel(Country.NAME);
addInputPanel(Country.LOCALNAME);
addInputPanel(Country.CAPITAL_FK);
addInputPanel(Country.CONTINENT);
addInputPanel(Country.REGION);
add(surfaceAreaIndYearPanel);
add(populationLifeExpectancyPanel);
add(gnpPanel);
addInputPanel(Country.GOVERNMENTFORM);
addInputPanel(Country.HEADOFSTATE);
add(createInputPanel(label("Avg. city population")
.horizontalAlignment(SwingConstants.CENTER)
.build(), averageCityPopulationField));
}
private EntityEditPanel createCapitalEditPanel() {
CityEditPanel capitalEditPanel = new CityEditPanel(new SwingEntityEditModel(City.TYPE, editModel().connectionProvider()));
if (editModel().entity().exists().get()) {
//if an existing country is selected, then we don't allow it to be changed
capitalEditPanel.editModel().value(City.COUNTRY_FK).set(editModel().entity().get());
//initialize the panel components, so we can configure the country component
capitalEditPanel.initialize();
//disable the country selection component
//and change the initial focus property
capitalEditPanel.disableCountryInput();
}
return capitalEditPanel;
}
}
CountryTablePanel
package is.codion.framework.demos.world.ui;
import is.codion.common.db.report.ReportException;
import is.codion.framework.demos.world.domain.api.World;
import is.codion.framework.demos.world.model.CountryTableModel;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import is.codion.swing.framework.ui.icon.FrameworkIcons;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.swing.JRViewer;
import java.awt.Dimension;
import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.PRINT;
final class CountryTablePanel extends EntityTablePanel {
CountryTablePanel(SwingEntityTableModel tableModel) {
super(tableModel, config -> config
.editable(attributes -> attributes.remove(World.Country.CAPITAL_FK)));
}
@Override
protected void setupControls() {
control(PRINT).set(Control.builder()
.command(this::viewCountryReport)
.name("Country report")
.enabled(tableModel().selection().empty().not())
.smallIcon(FrameworkIcons.instance().print())
.build());
}
private void viewCountryReport() {
Dialogs.progressWorkerDialog(this::fillCountryReport)
.owner(this)
.maximumProgress(tableModel().selection().count())
.stringPainted(true)
.onResult(this::viewReport)
.execute();
}
private JasperPrint fillCountryReport(ProgressReporter<String> progressReporter) throws ReportException {
CountryTableModel countryTableModel = tableModel();
return countryTableModel.fillCountryReport(progressReporter);
}
private void viewReport(JasperPrint countryReport) {
Dialogs.componentDialog(new JRViewer(countryReport))
.owner(this)
.modal(false)
.title("Country report")
.size(new Dimension(800, 600))
.show();
}
}
City
SQL
create table world.city (
id int not null,
name varchar(35) not null,
countrycode varchar(3) not null,
district varchar(20) not null,
population int not null,
location geometry(point),
constraint city_pk primary key (id)
);
Domain
API
interface City {
EntityType TYPE = DOMAIN.entityType("world.city");
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> COUNTRY_CODE = TYPE.stringColumn("countrycode");
Column<String> DISTRICT = TYPE.stringColumn("district");
Column<Integer> POPULATION = TYPE.integerColumn("population");
Column<Location> LOCATION = TYPE.column("location", Location.class);
ForeignKey COUNTRY_FK = TYPE.foreignKey("country", COUNTRY_CODE, Country.CODE);
}
record Location(double latitude, double longitude) implements Serializable {
@Override
public String toString() {
return "[" + latitude + "," + longitude + "]";
}
}
final class LocationComparator implements Comparator<Location>, Serializable {
@Override
public int compare(Location l1, Location l2) {
int result = Double.compare(l1.latitude(), l2.latitude());
if (result == 0) {
return Double.compare(l1.longitude(), l2.longitude());
}
return result;
}
}
final class CityValidator extends DefaultEntityValidator implements Serializable {
@Serial
private static final long serialVersionUID = 1;
@Override
public void validate(Entity city) throws ValidationException {
super.validate(city);
//after a call to super.validate() values that are not nullable
//(such as country and population) are guaranteed to be non-null
Entity country = city.get(City.COUNTRY_FK);
Integer cityPopulation = city.get(City.POPULATION);
Integer countryPopulation = country.get(Country.POPULATION);
if (countryPopulation != null && cityPopulation > countryPopulation) {
throw new ValidationException(City.POPULATION,
cityPopulation, "City population can not exceed country population");
}
}
}
Implementation
EntityDefinition city() {
return City.TYPE.define(
City.ID.define()
.primaryKey(),
City.NAME.define()
.column()
.caption("Name")
.searchable(true)
.nullable(false)
.maximumLength(35),
City.COUNTRY_CODE.define()
.column()
.nullable(false),
City.COUNTRY_FK.define()
.foreignKey()
.caption("Country"),
City.DISTRICT.define()
.column()
.caption("District")
.nullable(false)
.maximumLength(20),
City.POPULATION.define()
.column()
.caption("Population")
.nullable(false)
.numberFormatGrouping(true),
City.LOCATION.define()
.column()
.caption("Location")
.columnClass(String.class, new LocationConverter())
.comparator(new LocationComparator()))
.keyGenerator(sequence("world.city_seq"))
.validator(new CityValidator())
.orderBy(ascending(City.NAME))
.stringFactory(City.NAME)
.caption("City")
.build();
}
private static final class LocationConverter implements Converter<Location, String> {
@Override
public String toColumnValue(Location location,
Statement statement) {
return "POINT (" + location.longitude() + " " + location.latitude() + ")";
}
@Override
public Location fromColumnValue(String columnValue) {
String[] latLon = columnValue
.replace("POINT (", "")
.replace(")", "")
.split(" ");
return new Location(parseDouble(latLon[1]), parseDouble(latLon[0]));
}
}
Model
CityEditModel
package is.codion.framework.demos.world.model;
import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
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.framework.model.SwingEntityEditModel;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLEncoder;
import java.util.List;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
public final class CityEditModel extends SwingEntityEditModel {
private static final String OPENSTREETMAP_ORG_SEARCH = "https://nominatim.openstreetmap.org/search?q=";
public CityEditModel(EntityConnectionProvider connectionProvider) {
super(City.TYPE, connectionProvider);
initializeComboBoxModels(City.COUNTRY_FK);
}
public void populateLocation() throws IOException, DatabaseException, ValidationException {
Location location = lookupLocation(entity().get())
.orElseThrow(() -> new RuntimeException("Location not found for city: " + entity()));
value(City.LOCATION).set(location);
if (entity().modified().get()) {
update();
}
}
void populateLocation(Entity city) throws IOException, DatabaseException, ValidationException {
lookupLocation(city).ifPresent(location -> city.put(City.LOCATION, location));
if (city.modified()) {
update(List.of(city));
}
}
private static Optional<Location> lookupLocation(Entity city) throws IOException {
JSONArray jsonArray = toJSONArray(new URL(OPENSTREETMAP_ORG_SEARCH +
URLEncoder.encode(city.get(City.NAME), UTF_8) + "," +
URLEncoder.encode(city.get(City.COUNTRY_FK).get(Country.NAME), UTF_8) + "&format=json"));
if (!jsonArray.isEmpty()) {
JSONObject cityInformation = (JSONObject) jsonArray.get(0);
return Optional.of(new Location(cityInformation.getDouble("lat"), cityInformation.getDouble("lon")));
}
return Optional.empty();
}
private static JSONArray toJSONArray(URL url) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openConnection().getInputStream(), UTF_8))) {
return new JSONArray(reader.lines().collect(joining()));
}
}
}
CityTableModel
package is.codion.framework.demos.world.model;
import is.codion.common.event.Event;
import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressResultTask;
import is.codion.swing.framework.model.SwingEntityTableModel;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
public final class CityTableModel extends SwingEntityTableModel {
private final DefaultPieDataset<String> chartDataset = new DefaultPieDataset<>();
private final Event<Collection<Entity>> displayLocationEvent = Event.event();
private final State citiesWithoutLocationSelected = State.state();
CityTableModel(EntityConnectionProvider connectionProvider) {
super(new CityEditModel(connectionProvider));
selection().items().addConsumer(displayLocationEvent);
selection().indexes().addListener(this::updateCitiesWithoutLocationSelected);
refresher().success().addConsumer(this::refreshChartDataset);
}
public PieDataset<String> chartDataset() {
return chartDataset;
}
public PopulateLocationTask populateLocationTask() {
return new PopulateLocationTask();
}
public void addDisplayLocationConsumer(Consumer<Collection<Entity>> consumer) {
displayLocationEvent.addConsumer(consumer);
}
public StateObserver citiesWithoutLocationSelected() {
return citiesWithoutLocationSelected.observer();
}
private void refreshChartDataset(Collection<Entity> cities) {
chartDataset.clear();
cities.forEach(city -> chartDataset.setValue(city.get(City.NAME), city.get(City.POPULATION)));
}
private void updateCitiesWithoutLocationSelected() {
citiesWithoutLocationSelected.set(selection().items().get().stream()
.anyMatch(city -> city.isNull(City.LOCATION)));
}
public final class PopulateLocationTask implements ProgressResultTask<Void, String> {
private final State cancelled = State.state();
private final Collection<Entity> cities;
private PopulateLocationTask() {
cities = selection().items().get().stream()
.filter(city -> city.isNull(City.LOCATION))
.toList();
}
public int maximumProgress() {
return cities.size();
}
public StateObserver cancelled() {
return cancelled.not();
}
public void cancel() {
cancelled.set(true);
}
@Override
public Void execute(ProgressReporter<String> progressReporter) throws Exception {
Collection<Entity> updatedCities = new ArrayList<>();
CityEditModel editModel = editModel();
Iterator<Entity> citiesIterator = cities.iterator();
while (citiesIterator.hasNext() && !cancelled.get()) {
Entity city = citiesIterator.next();
progressReporter.publish(city.get(City.COUNTRY_FK).get(Country.NAME) + " - " + city.get(City.NAME));
editModel.populateLocation(city);
updatedCities.add(city);
progressReporter.report(updatedCities.size());
displayLocationEvent.accept(List.of(city));
}
displayLocationEvent.accept(selection().items().get());
return null;
}
}
}
UI
CityEditPanel
package is.codion.framework.demos.world.ui;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.state.State;
import is.codion.framework.demos.world.domain.api.World.City;
import is.codion.framework.demos.world.model.CityEditModel;
import is.codion.framework.demos.world.model.CityTableModel;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.icon.FrameworkIcons;
import org.jxmapviewer.JXMapKit;
import org.kordamp.ikonli.foundation.Foundation;
import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static java.util.stream.Collectors.toSet;
public final class CityEditPanel extends EntityEditPanel {
private final JXMapKit mapKit;
public CityEditPanel(SwingEntityEditModel editModel) {
super(editModel);
this.mapKit = null;
}
CityEditPanel(CityTableModel tableModel) {
super(tableModel.editModel());
this.mapKit = Maps.createMapKit();
tableModel.addDisplayLocationConsumer(this::displayLocation);
configureControls(config -> config
.control(Control.builder()
.command(this::populateLocation)
.enabled(State.and(active(),
editModel().entity().isNull(City.LOCATION),
editModel().entity().exists()))
.smallIcon(FrameworkIcons.instance().icon(Foundation.MAP))));
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(City.COUNTRY_FK);
createForeignKeyComboBox(City.COUNTRY_FK)
.preferredWidth(120);
createTextField(City.NAME);
createTextField(City.DISTRICT);
createTextField(City.POPULATION);
JPanel inputPanel = gridLayoutPanel(0, 1)
.add(createInputPanel(City.COUNTRY_FK))
.add(createInputPanel(City.NAME))
.add(createInputPanel(City.DISTRICT))
.add(createInputPanel(City.POPULATION))
.build();
JPanel centerPanel = gridLayoutPanel(1, 0)
.add(inputPanel)
.build();
if (mapKit != null) {
centerPanel.add(mapKit);
}
setLayout(borderLayout());
add(centerPanel, BorderLayout.CENTER);
}
void disableCountryInput() {
JComponent countryComponent = component(City.COUNTRY_FK).get();
countryComponent.setEnabled(false);
countryComponent.setFocusable(false);
initialFocusAttribute().set(City.NAME);
}
private void populateLocation() throws ValidationException, IOException, DatabaseException {
CityEditModel editModel = editModel();
editModel.populateLocation();
displayLocation(List.of(editModel.entity().get()));
}
private void displayLocation(Collection<Entity> cities) {
Maps.paintWaypoints(cities.stream()
.map(city -> city.optional(City.LOCATION))
.flatMap(Optional::stream)
.collect(toSet()), mapKit.getMainMap());
}
}
CityTablePanel
package is.codion.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.CityTableModel;
import is.codion.framework.demos.world.model.CityTableModel.PopulateLocationTask;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.ui.EntityTableCellRenderer;
import is.codion.swing.framework.ui.icon.FrameworkIcons;
import org.kordamp.ikonli.foundation.Foundation;
import java.awt.Color;
import java.util.Objects;
final class CityTablePanel extends ChartTablePanel {
CityTablePanel(CityTableModel tableModel) {
super(tableModel, tableModel.chartDataset(), "Cities", config -> config
.cellRenderer(City.POPULATION, EntityTableCellRenderer.builder(City.POPULATION, tableModel)
.foreground((table, city, attribute, population) ->
population > 1_000_000 ? Color.YELLOW : null)
.build())
.cellRenderer(City.NAME, EntityTableCellRenderer.builder(City.NAME, tableModel)
.foreground((table, city, attribute, name) ->
Objects.equals(city.get(City.ID), city.get(City.COUNTRY_FK).get(Country.CAPITAL)) ? Color.GREEN : null)
.build())
.editable(attributes -> attributes.remove(City.LOCATION)));
configurePopupMenu(config -> config.clear()
.control(createPopulateLocationControl())
.separator()
.defaults());
}
private Control createPopulateLocationControl() {
CityTableModel cityTableModel = tableModel();
return Control.builder()
.command(this::populateLocation)
.name("Populate location")
.enabled(cityTableModel.citiesWithoutLocationSelected())
.smallIcon(FrameworkIcons.instance().icon(Foundation.MAP))
.build();
}
private void populateLocation() {
CityTableModel tableModel = tableModel();
PopulateLocationTask task = tableModel.populateLocationTask();
Dialogs.progressWorkerDialog(task)
.owner(this)
.title("Populating locations")
.maximumProgress(task.maximumProgress())
.stringPainted(true)
.control(Control.builder()
.command(task::cancel)
.name("Cancel")
.enabled(task.cancelled().not()))
.onException(this::displayPopulateException)
.execute();
}
private void displayPopulateException(Exception exception) {
Dialogs.exceptionDialog()
.owner(this)
.title("Unable to populate location")
.show(exception);
}
}
CountryLanguage
SQL
create table world.countrylanguage (
countrycode varchar(3) not null,
language varchar(30) not null,
isofficial boolean default false not null,
percentage decimal(4,1) not null,
constraint countrylanguage_pk primary key (countrycode, language),
constraint countrylanguage_country_fk foreign key (countrycode) references world.country(code)
);
Domain
API
interface CountryLanguage {
EntityType TYPE = DOMAIN.entityType("world.countrylanguage");
Column<String> COUNTRY_CODE = TYPE.stringColumn("countrycode");
Column<String> LANGUAGE = TYPE.stringColumn("language");
Column<Boolean> IS_OFFICIAL = TYPE.booleanColumn("isofficial");
Column<Double> PERCENTAGE = TYPE.doubleColumn("percentage");
Attribute<Integer> NO_OF_SPEAKERS = TYPE.integerAttribute("noOfSpeakers");
ForeignKey COUNTRY_FK = TYPE.foreignKey("country_fk", COUNTRY_CODE, Country.CODE);
}
final class NoOfSpeakersProvider implements DerivedAttribute.Provider<Integer> {
@Serial
private static final long serialVersionUID = 1;
@Override
public Integer get(SourceValues values) {
Double percentage = values.get(CountryLanguage.PERCENTAGE);
Entity country = values.get(CountryLanguage.COUNTRY_FK);
if (percentage != null && country != null && country.isNotNull(Country.POPULATION)) {
return Double.valueOf(country.get(Country.POPULATION) * (percentage / 100)).intValue();
}
return null;
}
}
Implementation
EntityDefinition countryLanguage() {
return CountryLanguage.TYPE.define(
CountryLanguage.COUNTRY_CODE.define()
.primaryKey(0)
.updatable(true),
CountryLanguage.LANGUAGE.define()
.primaryKey(1)
.caption("Language")
.updatable(true),
CountryLanguage.COUNTRY_FK.define()
.foreignKey()
.caption("Country"),
CountryLanguage.IS_OFFICIAL.define()
.column()
.caption("Is official")
.columnHasDefaultValue(true)
.nullable(false),
CountryLanguage.NO_OF_SPEAKERS.define()
.derived(new NoOfSpeakersProvider(),
CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
.caption("No. of speakers")
.numberFormatGrouping(true),
CountryLanguage.PERCENTAGE.define()
.column()
.caption("Percentage")
.nullable(false)
.maximumFractionDigits(1)
.valueRange(0, 100))
.orderBy(OrderBy.builder()
.ascending(CountryLanguage.LANGUAGE)
.descending(CountryLanguage.PERCENTAGE)
.build())
.caption("Language")
.build();
}
Model
CountryLanguageTableModel
package is.codion.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;
import java.util.Collection;
public final class CountryLanguageTableModel extends SwingEntityTableModel {
private final DefaultPieDataset<String> chartDataset = new DefaultPieDataset<>();
CountryLanguageTableModel(EntityConnectionProvider connectionProvider) {
super(CountryLanguage.TYPE, connectionProvider);
editModel().initializeComboBoxModels(CountryLanguage.COUNTRY_FK);
refresher().success().addConsumer(this::refreshChartDataset);
}
public PieDataset<String> chartDataset() {
return chartDataset;
}
private void refreshChartDataset(Collection<Entity> countryLanguages) {
chartDataset.clear();
countryLanguages.forEach(countryLanguage ->
chartDataset.setValue(countryLanguage.get(CountryLanguage.LANGUAGE),
countryLanguage.get(CountryLanguage.NO_OF_SPEAKERS)));
}
}
UI
CountryLanguageEditPanel
package is.codion.framework.demos.world.ui;
import is.codion.framework.demos.world.domain.api.World.CountryLanguage;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import javax.swing.JPanel;
import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.gridLayout;
final class CountryLanguageEditPanel extends EntityEditPanel {
CountryLanguageEditPanel(SwingEntityEditModel editModel) {
super(editModel);
editModel.value(CountryLanguage.IS_OFFICIAL).edited().addListener(this::update);
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(CountryLanguage.COUNTRY_FK);
createForeignKeyComboBox(CountryLanguage.COUNTRY_FK)
.preferredWidth(120);
createTextField(CountryLanguage.LANGUAGE);
createCheckBox(CountryLanguage.IS_OFFICIAL);
createTextField(CountryLanguage.PERCENTAGE)
.columns(4);
JPanel percentageOfficialPanel = gridLayoutPanel(1, 2)
.add(createInputPanel(CountryLanguage.PERCENTAGE))
.add(createInputPanel(CountryLanguage.IS_OFFICIAL))
.build();
setLayout(gridLayout(0, 1));
addInputPanel(CountryLanguage.COUNTRY_FK);
addInputPanel(CountryLanguage.LANGUAGE);
add(percentageOfficialPanel);
}
}
CountryLanguageTablePanel
package is.codion.framework.demos.world.ui;
import is.codion.framework.demos.world.model.CountryLanguageTableModel;
final class CountryLanguageTablePanel extends ChartTablePanel {
CountryLanguageTablePanel(CountryLanguageTableModel tableModel) {
super(tableModel, tableModel.chartDataset(), "Languages");
}
}
Continents
Continent
Domain
API
interface Continent {
EntityType TYPE = DOMAIN.entityType("continent");
Column<String> NAME = TYPE.stringColumn("continent");
Column<Integer> SURFACE_AREA = TYPE.integerColumn("surface_area");
Column<Long> POPULATION = TYPE.longColumn("population");
Column<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleColumn("min_life_expectancy");
Column<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleColumn("max_life_expectancy");
Column<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerColumn("min_indep_year");
Column<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerColumn("max_indep_year");
Column<Double> GNP = TYPE.doubleColumn("gnp");
}
Implementation
EntityDefinition continent() {
return Continent.TYPE.define(
Continent.NAME.define()
.column()
.caption("Continent")
.groupBy(true),
Continent.SURFACE_AREA.define()
.column()
.caption("Surface area")
.expression("sum(surfacearea)")
.aggregate(true)
.numberFormatGrouping(true),
Continent.POPULATION.define()
.column()
.caption("Population")
.expression("sum(population)")
.aggregate(true)
.numberFormatGrouping(true),
Continent.MIN_LIFE_EXPECTANCY.define()
.column()
.caption("Min. life expectancy")
.expression("min(lifeexpectancy)")
.aggregate(true),
Continent.MAX_LIFE_EXPECTANCY.define()
.column()
.caption("Max. life expectancy")
.expression("max(lifeexpectancy)")
.aggregate(true),
Continent.MIN_INDEPENDENCE_YEAR.define()
.column()
.caption("Min. ind. year")
.expression("min(indepyear)")
.aggregate(true),
Continent.MAX_INDEPENDENCE_YEAR.define()
.column()
.caption("Max. ind. year")
.expression("max(indepyear)")
.aggregate(true),
Continent.GNP.define()
.column()
.caption("GNP")
.expression("sum(gnp)")
.aggregate(true)
.numberFormatGrouping(true))
.tableName("world.country")
.readOnly(true)
.caption("Continent")
.build();
}
Model
ContinentModel
package is.codion.framework.demos.world.model;
import is.codion.common.model.condition.ConditionModel;
import is.codion.common.model.condition.ConditionModel.Wildcard;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingDetailModelLink;
import is.codion.swing.framework.model.SwingEntityModel;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
import java.util.Collection;
public final class ContinentModel extends SwingEntityModel {
private final DefaultPieDataset<String> surfaceAreaDataset = new DefaultPieDataset<>();
private final DefaultPieDataset<String> populationDataset = new DefaultPieDataset<>();
private final DefaultPieDataset<String> gnpDataset = new DefaultPieDataset<>();
private final DefaultCategoryDataset lifeExpectancyDataset = new DefaultCategoryDataset();
ContinentModel(EntityConnectionProvider connectionProvider) {
super(Continent.TYPE, connectionProvider);
tableModel().refresher().success().addConsumer(this::refreshChartDatasets);
CountryModel countryModel = new CountryModel(connectionProvider);
addDetailModel(new CountryModelLink(countryModel)).active().set(true);
}
public PieDataset<String> populationDataset() {
return populationDataset;
}
public PieDataset<String> surfaceAreaDataset() {
return surfaceAreaDataset;
}
public PieDataset<String> gnpDataset() {
return gnpDataset;
}
public CategoryDataset lifeExpectancyDataset() {
return lifeExpectancyDataset;
}
private void refreshChartDatasets(Collection<Entity> continents) {
populationDataset.clear();
surfaceAreaDataset.clear();
gnpDataset.clear();
lifeExpectancyDataset.clear();
continents.forEach(continent -> {
String contientName = continent.get(Continent.NAME);
populationDataset.setValue(contientName, continent.get(Continent.POPULATION));
surfaceAreaDataset.setValue(contientName, continent.get(Continent.SURFACE_AREA));
gnpDataset.setValue(contientName, continent.get(Continent.GNP));
lifeExpectancyDataset.addValue(continent.get(Continent.MIN_LIFE_EXPECTANCY), "Lowest", contientName);
lifeExpectancyDataset.addValue(continent.get(Continent.MAX_LIFE_EXPECTANCY), "Highest", contientName);
});
}
private static final class CountryModel extends SwingEntityModel {
private CountryModel(EntityConnectionProvider connectionProvider) {
super(Country.TYPE, connectionProvider);
editModel().readOnly().set(true);
ConditionModel<?> continentConditionModel =
tableModel().queryModel().conditions().get(Country.CONTINENT);
continentConditionModel.wildcard().set(Wildcard.NONE);
continentConditionModel.caseSensitive().set(true);
}
}
private static final class CountryModelLink extends SwingDetailModelLink {
private CountryModelLink(SwingEntityModel detailModel) {
super(detailModel);
}
@Override
public void onSelection(Collection<Entity> selectedEntities) {
Collection<String> continentNames = Entity.values(Continent.NAME, selectedEntities);
if (detailModel().tableModel().queryModel().conditions().setInOperands(Country.CONTINENT, continentNames)) {
detailModel().tableModel().refresh();
}
}
}
}
UI
ContinentPanel
package is.codion.framework.demos.world.ui;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.model.ContinentModel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.EntityTablePanel;
import org.jfree.chart.ChartPanel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import java.awt.BorderLayout;
import java.awt.Dimension;
import static is.codion.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.component.Components.*;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
import static java.awt.event.KeyEvent.VK_1;
import static java.awt.event.KeyEvent.VK_2;
final class ContinentPanel extends EntityPanel {
ContinentPanel(ContinentModel continentModel) {
super(continentModel, new ContinentTablePanel(continentModel.tableModel()));
}
@Override
protected void initializeUI() {
ContinentModel model = model();
ChartPanel populationChartPanel = createPieChartPanel(this, model.populationDataset(), "Population");
ChartPanel surfaceAreaChartPanel = createPieChartPanel(this, model.surfaceAreaDataset(), "Surface area");
ChartPanel gnpChartPanel = createPieChartPanel(this, model.gnpDataset(), "GNP");
ChartPanel lifeExpectancyChartPanel = createBarChartPanel(this, model.lifeExpectancyDataset(), "Life expectancy", "Continent", "Years");
setPreferredHeight(lifeExpectancyChartPanel, 120);
Dimension pieChartSize = new Dimension(260, 260);
populationChartPanel.setPreferredSize(pieChartSize);
surfaceAreaChartPanel.setPreferredSize(pieChartSize);
gnpChartPanel.setPreferredSize(pieChartSize);
JPanel pieChartChartPanel = gridLayoutPanel(1, 3)
.add(populationChartPanel)
.add(surfaceAreaChartPanel)
.add(gnpChartPanel)
.build();
JPanel chartPanel = borderLayoutPanel()
.northComponent(lifeExpectancyChartPanel)
.centerComponent(pieChartChartPanel)
.build();
EntityTablePanel countryTablePanel =
new EntityTablePanel(model.detailModel(Country.TYPE).tableModel(),
config -> config
.includeConditions(false)
.includeToolBar(false));
setPreferredHeight(countryTablePanel, 300);
JTabbedPane tabbedPane = tabbedPane()
.tabBuilder("Charts", chartPanel)
.mnemonic(VK_1)
.add()
.tabBuilder("Countries", countryTablePanel.initialize())
.mnemonic(VK_2)
.add()
.build();
setLayout(borderLayout());
add(tablePanel().initialize(), BorderLayout.CENTER);
add(tabbedPane, BorderLayout.SOUTH);
setupKeyboardActions();
setupNavigation();
}
}
ContinentTablePanel
package is.codion.framework.demos.world.ui;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import javax.swing.JTable;
import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.REFRESH;
final class ContinentTablePanel extends EntityTablePanel {
ContinentTablePanel(SwingEntityTableModel tableModel) {
super(tableModel, config -> config.includeSouthPanel(false));
table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
configurePopupMenu(config -> config.clear()
.control(REFRESH));
}
}
Lookup
Lookup
Domain
API
interface Lookup {
EntityType TYPE = DOMAIN.entityType("world.country_city_lookup");
Column<String> COUNTRY_CODE = TYPE.stringColumn("country.code");
Column<String> COUNTRY_NAME = TYPE.stringColumn("country.name");
Column<String> COUNTRY_CONTINENT = TYPE.stringColumn("country.continent");
Column<String> COUNTRY_REGION = TYPE.stringColumn("country.region");
Column<Double> COUNTRY_SURFACEAREA = TYPE.doubleColumn("country.surfacearea");
Column<Integer> COUNTRY_INDEPYEAR = TYPE.integerColumn("country.indepyear");
Column<Integer> COUNTRY_POPULATION = TYPE.integerColumn("country.population");
Column<Double> COUNTRY_LIFEEXPECTANCY = TYPE.doubleColumn("country.lifeexpectancy");
Column<Double> COUNTRY_GNP = TYPE.doubleColumn("country.gnp");
Column<Double> COUNTRY_GNPOLD = TYPE.doubleColumn("country.gnpold");
Column<String> COUNTRY_LOCALNAME = TYPE.stringColumn("country.localname");
Column<String> COUNTRY_GOVERNMENTFORM = TYPE.stringColumn("country.governmentform");
Column<String> COUNTRY_HEADOFSTATE = TYPE.stringColumn("country.headofstate");
Column<String> COUNTRY_CODE2 = TYPE.stringColumn("country.code2");
Column<byte[]> COUNTRY_FLAG = TYPE.byteArrayColumn("country.flag");
Column<Integer> CITY_ID = TYPE.integerColumn("city.id");
Column<String> CITY_NAME = TYPE.stringColumn("city.name");
Column<String> CITY_DISTRICT = TYPE.stringColumn("city.district");
Column<Integer> CITY_POPULATION = TYPE.integerColumn("city.population");
Column<Location> CITY_LOCATION = TYPE.column("city.location", Location.class);
}
Implementation
EntityDefinition lookup() {
return Lookup.TYPE.define(
Lookup.COUNTRY_CODE.define()
.primaryKey(0)
.caption("Country code"),
Lookup.COUNTRY_NAME.define()
.column()
.caption("Country name"),
Lookup.COUNTRY_CONTINENT.define()
.column()
.caption("Continent")
.items(CONTINENT_ITEMS),
Lookup.COUNTRY_REGION.define()
.column()
.caption("Region"),
Lookup.COUNTRY_SURFACEAREA.define()
.column()
.caption("Surface area")
.numberFormatGrouping(true),
Lookup.COUNTRY_INDEPYEAR.define()
.column()
.caption("Indep. year"),
Lookup.COUNTRY_POPULATION.define()
.column()
.caption("Country population")
.numberFormatGrouping(true),
Lookup.COUNTRY_LIFEEXPECTANCY.define()
.column()
.caption("Life expectancy"),
Lookup.COUNTRY_GNP.define()
.column()
.caption("GNP")
.numberFormatGrouping(true),
Lookup.COUNTRY_GNPOLD.define()
.column()
.caption("GNP old")
.numberFormatGrouping(true),
Lookup.COUNTRY_LOCALNAME.define()
.column()
.caption("Local name"),
Lookup.COUNTRY_GOVERNMENTFORM.define()
.column()
.caption("Government form"),
Lookup.COUNTRY_HEADOFSTATE.define()
.column()
.caption("Head of state"),
Lookup.COUNTRY_FLAG.define()
.column()
.caption("Flag")
.lazy(true),
Lookup.COUNTRY_CODE2.define()
.column()
.caption("Code2"),
Lookup.CITY_ID.define()
.primaryKey(1),
Lookup.CITY_NAME.define()
.column()
.caption("City"),
Lookup.CITY_DISTRICT.define()
.column()
.caption("District"),
Lookup.CITY_POPULATION.define()
.column()
.caption("City population")
.numberFormatGrouping(true),
Lookup.CITY_LOCATION.define()
.column()
.caption("City location")
.columnClass(String.class, new LocationConverter())
.comparator(new LocationComparator()))
.selectQuery(SelectQuery.builder()
.from("world.country LEFT OUTER JOIN world.city ON city.countrycode = country.code")
.build())
.orderBy(OrderBy.builder()
.ascending(Lookup.COUNTRY_NAME)
.descending(Lookup.CITY_POPULATION)
.build())
.readOnly(true)
.caption("Lookup")
.build();
}
UI
LookupTablePanel
package is.codion.framework.demos.world.ui;
import is.codion.common.state.State;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.json.domain.EntityObjectMapper;
import is.codion.swing.common.ui.Utilities;
import is.codion.swing.common.ui.component.button.ToggleButtonType;
import is.codion.swing.common.ui.component.table.ConditionPanel.ConditionView;
import is.codion.swing.common.ui.control.Control;
import is.codion.swing.common.ui.control.Controls;
import is.codion.swing.common.ui.control.ToggleControl;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import is.codion.swing.framework.ui.icon.FrameworkIcons;
import org.jxmapviewer.JXMapKit;
import org.kordamp.ikonli.foundation.Foundation;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static is.codion.framework.demos.world.ui.LookupTablePanel.ExportFormat.CSV;
import static is.codion.framework.demos.world.ui.LookupTablePanel.ExportFormat.JSON;
import static is.codion.framework.json.domain.EntityObjectMapper.entityObjectMapper;
import static is.codion.swing.common.ui.component.Components.scrollPane;
import static is.codion.swing.common.ui.component.Components.toolBar;
import static is.codion.swing.framework.ui.EntityTablePanel.ControlKeys.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;
final class LookupTablePanel extends EntityTablePanel {
private static final Dimension DEFAULT_MAP_SIZE = new Dimension(400, 400);
private static final FrameworkIcons ICONS = FrameworkIcons.instance();
enum ExportFormat {
CSV {
@Override
public String defaultFileName() {
return "export.csv";
}
},
JSON {
@Override
public String defaultFileName() {
return "export.json";
}
};
public abstract String defaultFileName();
}
private final EntityObjectMapper objectMapper = entityObjectMapper(tableModel().entities());
private final State columnSelectionPanelVisible = State.state(true);
private final State mapDialogVisible = State.builder()
.consumer(this::setMapDialogVisible)
.build();
private final Control toggleMapControl = Control.builder()
.toggle(mapDialogVisible)
.smallIcon(ICONS.icon(Foundation.MAP))
.name("Show map")
.build();
private final JScrollPane columnSelectionScrollPane = scrollPane(createColumnSelectionToolBar())
.verticalUnitIncrement(16)
.build();
private final JXMapKit mapKit = Maps.createMapKit();
private JDialog mapKitDialog;
LookupTablePanel(SwingEntityTableModel lookupModel) {
super(lookupModel, config -> config
.showRefreshProgressBar(true)
.conditionView(ConditionView.SIMPLE));
columnSelectionPanelVisible.addConsumer(this::setColumnSelectionPanelVisible);
table().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
configurePopupMenuAndToolBar();
bindEvents();
}
@Override
public void updateUI() {
super.updateUI();
Utilities.updateComponentTreeUI(mapKit);
}
@Override
protected void setupControls() {
control(CLEAR).set(Control.builder()
.command(this::clearTableAndConditions)
.name("Clear")
.mnemonic('C')
.smallIcon(ICONS.clear())
.build());
}
@Override
protected void layoutPanel(JComponent tableComponent, JPanel southPanel) {
super.layoutPanel(tableComponent, southPanel);
add(columnSelectionScrollPane, BorderLayout.EAST);
}
private void configurePopupMenuAndToolBar() {
configurePopupMenu(config -> config.clear()
.control(REFRESH)
.control(CLEAR)
.separator()
.control(Controls.builder()
.name("Export")
.smallIcon(ICONS.icon(Foundation.PAGE_EXPORT))
.control(Control.builder()
.command(this::exportCSV)
.name("CSV..."))
.control(Control.builder()
.command(this::exportJSON)
.name("JSON...")))
.control(Controls.builder()
.name("Import")
.smallIcon(ICONS.icon(Foundation.PAGE_ADD))
.control(Control.builder()
.command(this::importJSON)
.name("JSON...")))
.separator()
.control(toggleMapControl)
.separator()
.control(Controls.builder()
.name("Columns")
.smallIcon(FrameworkIcons.instance().columns())
.control(Control.builder()
.toggle(columnSelectionPanelVisible)
.name("Select")
.build())
.control(control(RESET_COLUMNS).get())
.control(control(SELECT_AUTO_RESIZE_MODE).get()))
.separator()
.control(CONDITION_CONTROLS)
.control(COPY_CONTROLS));
configureToolBar(config -> config.clear()
.control(toggleMapControl)
.separator()
.defaults());
}
private void bindEvents() {
tableModel().items().visible().addListener(this::displayCityLocations);
tableModel().selection().indexes().addListener(this::displayCityLocations);
}
private void displayCityLocations() {
if (mapKit.isShowing()) {
Collection<Entity> entities = tableModel().selection().empty().get() ?
tableModel().items().visible().get() :
tableModel().selection().items().get();
Maps.paintWaypoints(entities.stream()
.map(entity -> entity.optional(Lookup.CITY_LOCATION))
.flatMap(Optional::stream)
.collect(toSet()), mapKit.getMainMap());
}
}
private void setMapDialogVisible(boolean visible) {
if (mapKitDialog == null) {
mapKitDialog = Dialogs.componentDialog(mapKit)
.owner(this)
.modal(false)
.title("World Map")
.size(DEFAULT_MAP_SIZE)
.onShown(dialog -> displayCityLocations())
.onClosed(e -> mapDialogVisible.set(false))
.build();
}
mapKitDialog.setVisible(visible);
}
private void exportCSV() {
export(CSV);
}
private void exportJSON() {
export(JSON);
}
private void export(ExportFormat format) {
File fileToSave = Dialogs.fileSelectionDialog()
.owner(this)
.selectFileToSave(format.defaultFileName());
Dialogs.progressWorkerDialog(() -> export(fileToSave, format))
.owner(this)
.title("Exporting data")
.onResult("Export successful")
.onException("Export failed")
.execute();
}
private void export(File file, ExportFormat format) throws IOException {
requireNonNull(file);
requireNonNull(format);
switch (format) {
case CSV:
exportCSV(file);
break;
case JSON:
exportJSON(file);
break;
default:
throw new IllegalArgumentException("Unknown export format: " + format);
}
}
private void exportCSV(File file) throws IOException {
Files.write(file.toPath(), List.of(table().export()
.delimiter(',')
.selected(true)
.get()));
}
private void exportJSON(File file) throws IOException {
Collection<Entity> entities = tableModel().selection().empty().get() ?
tableModel().items().get() :
tableModel().selection().items().get();
Files.write(file.toPath(), objectMapper.writeValueAsString(entities).getBytes(UTF_8));
}
private void importJSON() throws IOException {
importJSON(Dialogs.fileSelectionDialog()
.owner(this)
.fileFilter(new FileNameExtensionFilter("JSON", "json"))
.selectFile());
}
public void importJSON(File file) throws IOException {
List<Entity> entities = objectMapper.deserializeEntities(
String.join("\n", Files.readAllLines(file.toPath())));
clearTableAndConditions();
tableModel().items().visible().addItemsAt(0, entities);
tableModel().items().visible().sort();
}
private JToolBar createColumnSelectionToolBar() {
Controls toggleColumnsControls = table().createToggleColumnsControls();
return toolBar(Controls.builder()
.control(createSelectAllColumnsControl(toggleColumnsControls))
.separator()
.actions(toggleColumnsControls.actions()))
.floatable(false)
.orientation(SwingConstants.VERTICAL)
.toggleButtonType(ToggleButtonType.CHECKBOX)
.includeButtonText(true)
.build();
}
private void setColumnSelectionPanelVisible(boolean visible) {
columnSelectionScrollPane.setVisible(visible);
revalidate();
}
private void clearTableAndConditions() {
tableModel().items().clear();
tableModel().queryModel().conditions().clear();
}
private static Control createSelectAllColumnsControl(Controls toggleColumnsControls) {
return Control.builder()
.command(() -> toggleColumnsControls.actions().stream()
.map(ToggleControl.class::cast)
.forEach(toggleControl -> toggleControl.value().set(true)))
.name("Select all")
.smallIcon(ICONS.icon(Foundation.CHECK))
.build();
}
}
Common classes
UI
package is.codion.framework.demos.world.ui;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityTablePanel;
import org.jfree.chart.ChartPanel;
import org.jfree.data.general.PieDataset;
import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.util.function.Consumer;
import static is.codion.framework.demos.world.ui.ChartPanels.createPieChartPanel;
import static is.codion.swing.common.ui.component.Components.borderLayoutPanel;
import static is.codion.swing.common.ui.component.Components.tabbedPane;
import static java.awt.event.KeyEvent.VK_1;
import static java.awt.event.KeyEvent.VK_2;
abstract class ChartTablePanel extends EntityTablePanel {
private final ChartPanel chartPanel;
protected ChartTablePanel(SwingEntityTableModel tableModel, PieDataset<String> chartDataset,
String chartTitle) {
this(tableModel, chartDataset, chartTitle, config -> {});
}
protected ChartTablePanel(SwingEntityTableModel tableModel, PieDataset<String> chartDataset,
String chartTitle, Consumer<Config> configuration) {
super(tableModel, configuration);
setPreferredSize(new Dimension(200, 200));
chartPanel = createPieChartPanel(this, chartDataset, chartTitle);
}
@Override
protected final void layoutPanel(JComponent tableComponent, JPanel southPanel) {
super.layoutPanel(tabbedPane()
.tabBuilder("Table", borderLayoutPanel()
.centerComponent(tableComponent)
.southComponent(southPanel)
.build())
.mnemonic(VK_1)
.add()
.tabBuilder("Chart", chartPanel)
.mnemonic(VK_2)
.add()
.build(), southPanel);
}
}
package is.codion.framework.demos.world.ui;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.general.PieDataset;
import javax.swing.JComponent;
import javax.swing.UIManager;
import java.awt.Color;
import static org.jfree.chart.ChartFactory.createBarChart;
import static org.jfree.chart.ChartFactory.createPieChart;
final class ChartPanels {
private ChartPanels() {}
static ChartPanel createPieChartPanel(JComponent parent, PieDataset<String> dataset, String title) {
JFreeChart chart = createPieChart(title, dataset);
chart.removeLegend();
linkColors(parent, chart);
return new ChartPanel(chart);
}
static ChartPanel createBarChartPanel(JComponent parent, CategoryDataset dataset, String title,
String categoryLabel, String valueLabel) {
JFreeChart chart = createBarChart(title, categoryLabel, valueLabel, dataset);
linkColors(parent, chart);
return new ChartPanel(chart);
}
private static void linkColors(JComponent parent, JFreeChart chart) {
setColors(chart, parent.getBackground());
parent.addPropertyChangeListener("background", evt ->
setColors(chart, (Color) evt.getNewValue()));
}
private static void setColors(JFreeChart chart, Color backgroundColor) {
chart.setBackgroundPaint(backgroundColor);
Plot plot = chart.getPlot();
plot.setBackgroundPaint(backgroundColor);
Color textFieldForeground = UIManager.getColor("TextField.foreground");
if (plot instanceof PiePlot<?> piePlot) {
piePlot.setLabelBackgroundPaint(textFieldForeground);
piePlot.setLabelPaint(backgroundColor);
}
if (plot instanceof CategoryPlot categoryPlot) {
categoryPlot.getDomainAxis().setLabelPaint(textFieldForeground);
categoryPlot.getRangeAxis().setLabelPaint(textFieldForeground);
categoryPlot.getDomainAxis().setTickLabelPaint(textFieldForeground);
categoryPlot.getRangeAxis().setTickLabelPaint(textFieldForeground);
}
LegendTitle legend = chart.getLegend();
if (legend != null) {
legend.setBackgroundPaint(backgroundColor);
legend.setItemPaint(textFieldForeground);
}
chart.getTitle().setPaint(textFieldForeground);
}
}
package is.codion.framework.demos.world.ui;
import is.codion.framework.demos.world.domain.api.World.Location;
import org.jxmapviewer.JXMapKit;
import org.jxmapviewer.JXMapViewer;
import org.jxmapviewer.OSMTileFactoryInfo;
import org.jxmapviewer.viewer.DefaultTileFactory;
import org.jxmapviewer.viewer.DefaultWaypoint;
import org.jxmapviewer.viewer.GeoPosition;
import org.jxmapviewer.viewer.Waypoint;
import org.jxmapviewer.viewer.WaypointPainter;
import javax.swing.SwingUtilities;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
final class Maps {
private static final int MIN_ZOOM = 19;
private static final int SINGLE_WAYPOINT_ZOOM_LEVEL = 15;
private Maps() {}
static JXMapKit createMapKit() {
JXMapKit mapKit = new JXMapKit();
mapKit.setTileFactory(new DefaultTileFactory(new OSMTileFactoryInfo()));
mapKit.setMiniMapVisible(false);
mapKit.setZoomSliderVisible(false);
mapKit.setZoomButtonsVisible(false);
mapKit.getMainMap().setZoom(MIN_ZOOM);
mapKit.getMainMap().setOverlayPainter(new WaypointPainter<>());
return mapKit;
}
static void paintWaypoints(Set<Location> positions, JXMapViewer mapViewer) {
Set<GeoPosition> geoPositions = positions.stream()
.map(location -> new GeoPosition(location.latitude(), location.longitude()))
.collect(toSet());
WaypointPainter<Waypoint> overlayPainter = (WaypointPainter<Waypoint>) mapViewer.getOverlayPainter();
overlayPainter.setWaypoints(geoPositions.stream()
.map(DefaultWaypoint::new)
.collect(toSet()));
SwingUtilities.invokeLater(() -> {
if (geoPositions.isEmpty()) {
mapViewer.setZoom(MIN_ZOOM);
mapViewer.repaint();
}
else if (geoPositions.size() == 1) {
mapViewer.setZoom(0);
mapViewer.setCenterPosition(geoPositions.iterator().next());
mapViewer.setZoom(SINGLE_WAYPOINT_ZOOM_LEVEL);
}
else {
mapViewer.setZoom(0);
mapViewer.zoomToBestFit(geoPositions, 1);
}
});
}
}
Application
WorldAppModel
package is.codion.framework.demos.world.model;
import is.codion.common.version.Version;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.swing.framework.model.SwingEntityApplicationModel;
import is.codion.swing.framework.model.SwingEntityModel;
public final class WorldAppModel extends SwingEntityApplicationModel {
public static final Version VERSION = Version.parse(WorldAppModel.class, "/version.properties");
public WorldAppModel(EntityConnectionProvider connectionProvider) {
super(connectionProvider, VERSION);
CountryModel countryModel = new CountryModel(connectionProvider);
SwingEntityModel lookupModel = new SwingEntityModel(Lookup.TYPE, connectionProvider);
ContinentModel continentModel = new ContinentModel(connectionProvider);
countryModel.tableModel().refresh();
continentModel.tableModel().refresh();
addEntityModels(countryModel, lookupModel, continentModel);
}
}
UI
WorldAppPanel
package is.codion.framework.demos.world.ui;
import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.framework.demos.world.domain.api.World;
import is.codion.framework.demos.world.domain.api.World.Continent;
import is.codion.framework.demos.world.domain.api.World.Country;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.demos.world.model.ContinentModel;
import is.codion.framework.demos.world.model.CountryModel;
import is.codion.framework.demos.world.model.WorldAppModel;
import is.codion.swing.common.ui.component.table.FilterTableCellRenderer;
import is.codion.swing.common.ui.laf.LookAndFeelProvider;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.ReferentialIntegrityErrorHandling;
import is.codion.swing.framework.ui.icon.FrameworkIcons;
import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes;
import org.kordamp.ikonli.foundation.Foundation;
import javax.swing.SwingConstants;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public final class WorldAppPanel extends EntityApplicationPanel<WorldAppModel> {
private static final String DEFAULT_FLAT_LOOK_AND_FEEL = "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme";
public WorldAppPanel(WorldAppModel appModel) {
super(appModel);
FrameworkIcons.instance().add(Foundation.MAP, Foundation.PAGE_EXPORT, Foundation.PAGE_ADD, Foundation.CHECK);
}
@Override
protected List<EntityPanel> createEntityPanels() {
CountryModel countryModel = applicationModel().entityModel(Country.TYPE);
CountryPanel countryPanel = new CountryPanel(countryModel);
ContinentModel continentModel = applicationModel().entityModel(Continent.TYPE);
ContinentPanel continentPanel = new ContinentPanel(continentModel);
SwingEntityModel lookupModel = applicationModel().entityModel(Lookup.TYPE);
EntityPanel lookupPanel = new EntityPanel(lookupModel,
new LookupTablePanel(lookupModel.tableModel()));
return List.of(countryPanel, continentPanel, lookupPanel);
}
public static void main(String[] args) throws CancelException {
Locale.setDefault(new Locale("en", "EN"));
Arrays.stream(FlatAllIJThemes.INFOS).forEach(LookAndFeelProvider::addLookAndFeel);
EntityPanel.Config.TOOLBAR_CONTROLS.set(true);
FilterTableCellRenderer.NUMERICAL_HORIZONTAL_ALIGNMENT.set(SwingConstants.CENTER);
ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING.set(ReferentialIntegrityErrorHandling.DISPLAY_DEPENDENCIES);
EntityApplicationPanel.builder(WorldAppModel.class, WorldAppPanel.class)
.applicationName("World")
.domainType(World.DOMAIN)
.applicationVersion(WorldAppModel.VERSION)
.defaultLookAndFeelClassName(DEFAULT_FLAT_LOOK_AND_FEEL)
.defaultLoginUser(User.parse("scott:tiger"))
.start();
}
}
Domain unit test
package is.codion.framework.demos.world.domain;
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.demos.world.domain.api.World.CountryLanguage;
import is.codion.framework.demos.world.domain.api.World.Lookup;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.framework.domain.test.DefaultEntityFactory;
import is.codion.framework.domain.test.DomainTest;
import org.junit.jupiter.api.Test;
import java.util.Optional;
public final class WorldImplTest extends DomainTest {
public WorldImplTest() {
super(new WorldImpl(), WorldEntityFactory::new);
}
@Test
void country() throws DatabaseException {
test(Country.TYPE);
}
@Test
void city() throws DatabaseException {
test(City.TYPE);
}
@Test
void countryLanguage() throws DatabaseException {
test(CountryLanguage.TYPE);
}
@Test
void lookup() throws DatabaseException {
connection().selectSingle(Lookup.CITY_NAME.equalTo("Genova"));
}
private static final class WorldEntityFactory extends DefaultEntityFactory {
private WorldEntityFactory(EntityConnection connection) {
super(connection);
}
@Override
public Entity entity(EntityType entityType) throws DatabaseException {
Entity entity = super.entity(entityType);
if (entityType.equals(Country.TYPE)) {
entity.put(Country.CODE, "XYZ");
entity.put(Country.CONTINENT, "Asia");
}
else if (entityType.equals(City.TYPE)) {
entity.remove(City.LOCATION);
}
return entity;
}
@Override
public void modify(Entity entity) throws DatabaseException {
super.modify(entity);
if (entity.entityType().equals(Country.TYPE)) {
entity.put(Country.CONTINENT, "Europe");
}
else if (entity.entityType().equals(City.TYPE)) {
entity.put(City.LOCATION, null);
}
}
@Override
public Optional<Entity> entity(ForeignKey foreignKey) throws DatabaseException {
if (foreignKey.referencedType().equals(Country.TYPE)) {
return Optional.of(entities().builder(Country.TYPE)
.with(Country.CODE, "ISL")
.build());
}
if (foreignKey.referencedType().equals(City.TYPE)) {
return Optional.of(entities().builder(City.TYPE)
.with(City.ID, 1449)
.build());
}
return super.entity(foreignKey);
}
}
}
Module Info
Domain
API
/**
* Domain API.
*/
module is.codion.framework.demos.world.domain.api {
requires transitive is.codion.framework.domain;
requires transitive is.codion.framework.db.core;
exports is.codion.framework.demos.world.domain.api;
}
Implementation
/**
* Domain implementation.
*/
module is.codion.framework.demos.world.domain {
requires transitive is.codion.framework.demos.world.domain.api;
exports is.codion.framework.demos.world.domain;
provides is.codion.framework.domain.Domain
with is.codion.framework.demos.world.domain.WorldImpl;
}
Client
/**
* Client.
*/
module is.codion.framework.demos.world.client {
requires is.codion.framework.json.domain;
requires is.codion.swing.framework.ui;
requires is.codion.plugin.jasperreports;
requires is.codion.framework.demos.world.domain.api;
requires com.formdev.flatlaf.intellijthemes;
requires org.kordamp.ikonli.foundation;
requires org.jfree.jfreechart;
requires net.sf.jasperreports.core;
requires net.sf.jasperreports.pdf;
requires net.sf.jasperreports.fonts;
requires org.apache.commons.logging;
requires com.github.librepdf.openpdf;
requires org.jxmapviewer.jxmapviewer2;
requires org.json;
exports is.codion.framework.demos.world.ui
to is.codion.swing.framework.ui;
//for loading reports from classpath
opens is.codion.framework.demos.world.model;
}