1. Project
1.1. Building
The Codion framework is built with Gradle and includes the Gradle Wrapper with a toolchain defined, so assuming you have cloned the repository and worked your way into the project directory you can build the framework by running the following command.
gradlew build
Note
|
This may take a few minutes, depending on the machine. |
To install the Codion framework into your local Maven repository run the following command.
gradlew publishToMavenLocal
1.2. Running the demos
Note
|
The demos use an embedded in-memory database, so changes to data do not persist. |
1.2.1. Local database connection
You can start by running a client from one of the demo projects (employees, chinook, petstore or world) with a local database connection.
gradlew demo-chinook:runClientLocal
1.2.2. Remote database connection
In order to run a client with a remote or http connection the remote server must be started first.
gradlew demo-server:run
To run a demo client with a remote connection use the following command.
gradlew demo-chinook:runClientRMI
You can run the Server Monitor application to see how the server is behaving, with the following command.
gradlew demo-server-monitor:run
Note
|
The client handles server restarts gracefully, you can try shutting down the server via the Server Monitor, play around in the client until you get a 'Connection refused' exception. After you restart the server the client simply reconnects and behaves as if nothing happened. |
1.3. Code style
After having wrestled with many code formatters and never being fully satisfied with the result, I’ve wound up relying on IntelliJ for code formatting.
The project has a defined code style which can be found in the .idea/codeStyles
folder.
2. Architecture
The Codion framework is based on a three tiered architecture.
-
Database layer
-
Model layer
-
UI layer
2.1. Database layer
The EntityConnection class defines the database layer. See Manual:EntityConnection
2.2. Model layer
The EntityModel class defines the model layer. See Manual:EntityModel.
2.3. UI layer
The EntityPanel class defines the UI layer. See Manual:EntityPanel.
3. Client
3.1. Features
-
Lightweight client with a simple synchronous event model
-
Provides a practically mouse free user experience
-
Graceful handling of network outages and server restarts
-
Clear separation between model and UI
-
Easy to use load testing harness provided for applications
-
UI data bindings for most common components provided by the framework
-
Implementing data bindings for new components is made simple with building blocks provided by the framework
-
The default UI layout is a simple and intuitive “waterfall” master-detail view
-
Extensive searching and filtering capabilities
-
Flexible keyboard-centric UI based on tab and split panes, detachable panels and toolbars
-
Detailed logging of client actions
3.3. Architecture
3.3.3. Assembly
EntityModel
/**
* Creates a SwingEntityModel based on the {@link Artist#TYPE} entity
* with a detail model based on {@link Album#TYPE}
* @param connectionProvider the connection provider
*/
static SwingEntityModel artistModel(EntityConnectionProvider connectionProvider) {
// create a default edit model
SwingEntityEditModel artistEditModel =
new SwingEntityEditModel(Artist.TYPE, connectionProvider);
// create a default table model, wrapping the edit model
SwingEntityTableModel artistTableModel =
new SwingEntityTableModel(artistEditModel);
// create a default model wrapping the table model
SwingEntityModel artistModel =
new SwingEntityModel(artistTableModel);
// Note that this does the same as the above, that is, creates
// a SwingEntityModel with a default edit and table model
SwingEntityModel albumModel =
new SwingEntityModel(Album.TYPE, connectionProvider);
artistModel.detailModels().add(albumModel);
return artistModel;
}
EntityPanel
/**
* Creates a EntityPanel based on the {@link Artist#TYPE} entity
* with a detail panel based on {@link Album#TYPE}
* @param connectionProvider the connection provider
*/
static EntityPanel artistPanel(EntityConnectionProvider connectionProvider) {
// create the EntityModel to base the panel on (calling the above method)
SwingEntityModel artistModel = artistModel(connectionProvider);
// the edit model
SwingEntityEditModel artistEditModel = artistModel.editModel();
// the table model
SwingEntityTableModel artistTableModel = artistModel.tableModel();
// the album detail model
SwingEntityModel albumModel = artistModel.detailModels().get(Album.TYPE);
// create a EntityEditPanel instance, based on the artist edit model
EntityEditPanel artistEditPanel = new EntityEditPanel(artistEditModel) {
@Override
protected void initializeUI() {
createTextField(Artist.NAME).columns(15);
addInputPanel(Artist.NAME);
}
};
// create a EntityTablePanel instance, based on the artist table model
EntityTablePanel artistTablePanel = new EntityTablePanel(artistTableModel);
// create a EntityPanel instance, based on the artist model and
// the edit and table panels from above
EntityPanel artistPanel = new EntityPanel(artistModel, artistEditPanel, artistTablePanel);
// create a new EntityPanel, without an edit panel and
// with a default EntityTablePanel
EntityPanel albumPanel = new EntityPanel(albumModel);
artistPanel.detailPanels().add(albumPanel);
return artistPanel;
}
3.3.4. Full Example
Show code
package is.codion.demos.chinook.tutorial;
import is.codion.common.db.database.Database;
import is.codion.common.user.User;
import is.codion.demos.chinook.domain.ChinookImpl;
import is.codion.demos.chinook.domain.api.Chinook.Album;
import is.codion.demos.chinook.domain.api.Chinook.Artist;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.EntityTablePanel;
/**
* When running this make sure the chinook demo module directory is the
* working directory, due to a relative path to a db init script
*/
public final class ClientArchitecture {
// tag::entityModel[]
/**
* Creates a SwingEntityModel based on the {@link Artist#TYPE} entity
* with a detail model based on {@link Album#TYPE}
* @param connectionProvider the connection provider
*/
static SwingEntityModel artistModel(EntityConnectionProvider connectionProvider) {
// create a default edit model
SwingEntityEditModel artistEditModel =
new SwingEntityEditModel(Artist.TYPE, connectionProvider);
// create a default table model, wrapping the edit model
SwingEntityTableModel artistTableModel =
new SwingEntityTableModel(artistEditModel);
// create a default model wrapping the table model
SwingEntityModel artistModel =
new SwingEntityModel(artistTableModel);
// Note that this does the same as the above, that is, creates
// a SwingEntityModel with a default edit and table model
SwingEntityModel albumModel =
new SwingEntityModel(Album.TYPE, connectionProvider);
artistModel.detailModels().add(albumModel);
return artistModel;
}
// end::entityModel[]
// tag::entityPanel[]
/**
* Creates a EntityPanel based on the {@link Artist#TYPE} entity
* with a detail panel based on {@link Album#TYPE}
* @param connectionProvider the connection provider
*/
static EntityPanel artistPanel(EntityConnectionProvider connectionProvider) {
// create the EntityModel to base the panel on (calling the above method)
SwingEntityModel artistModel = artistModel(connectionProvider);
// the edit model
SwingEntityEditModel artistEditModel = artistModel.editModel();
// the table model
SwingEntityTableModel artistTableModel = artistModel.tableModel();
// the album detail model
SwingEntityModel albumModel = artistModel.detailModels().get(Album.TYPE);
// create a EntityEditPanel instance, based on the artist edit model
EntityEditPanel artistEditPanel = new EntityEditPanel(artistEditModel) {
@Override
protected void initializeUI() {
createTextField(Artist.NAME).columns(15);
addInputPanel(Artist.NAME);
}
};
// create a EntityTablePanel instance, based on the artist table model
EntityTablePanel artistTablePanel = new EntityTablePanel(artistTableModel);
// create a EntityPanel instance, based on the artist model and
// the edit and table panels from above
EntityPanel artistPanel = new EntityPanel(artistModel, artistEditPanel, artistTablePanel);
// create a new EntityPanel, without an edit panel and
// with a default EntityTablePanel
EntityPanel albumPanel = new EntityPanel(albumModel);
artistPanel.detailPanels().add(albumPanel);
return artistPanel;
}
// end::entityPanel[]
public static void main(String[] args) {
// Configure the database
Database.DATABASE_URL.set("jdbc:h2:mem:h2db");
Database.DATABASE_INIT_SCRIPTS.set("src/main/sql/create_schema.sql");
// initialize a connection provider, this class is responsible
// for supplying a valid connection or throwing an exception
// in case a connection can not be established
EntityConnectionProvider connectionProvider =
LocalEntityConnectionProvider.builder()
.domain(new ChinookImpl())
.user(User.parse("scott:tiger"))
.build();
EntityPanel artistPanel = artistPanel(connectionProvider);
// lazy initialization
artistPanel.initialize();
// fetch data from the database
artistPanel.model().tableModel().items().refresh();
// uncomment the below line to display the panel
// displayInDialog(null, artistPanel, "Artists");
connectionProvider.close();
}
}
4. Testing Strategy & Patterns
4.1. Overview
Codion applications follow a layered testing approach, with each layer providing specific testing capabilities:
-
Domain Testing - Validates entity definitions, relationships, and domain logic
-
Model Testing - Tests business logic in edit and table models
-
Integration Testing - Verifies end-to-end functionality with database operations
4.3. Domain Testing
The DomainTest
base class provides comprehensive testing for entity definitions.
4.3.1. Basic Domain Test
public class ChinookTest extends DomainTest {
public ChinookTest() {
super(new ChinookImpl(), ChinookEntityFactory::new);
}
@Test
void album() {
test(Album.TYPE);
}
@Test
void artist() {
test(Artist.TYPE);
}
}
The test(EntityType)
method automatically verifies:
-
Entity can be instantiated
-
All attributes are correctly defined
-
Primary keys work correctly
-
Foreign key relationships are valid
-
Insert, update, delete, and select operations succeed
-
Entity validation rules are enforced
4.3.2. Custom Entity Factory
For entities with complex validation rules or required relationships, provide a custom EntityFactory:
public class ChinookEntityFactory extends DefaultEntityFactory {
@Override
public Entity entity(EntityType entityType) {
if (entityType.equals(InvoiceLine.TYPE)) {
return createInvoiceLine();
}
return super.entity(entityType);
}
private Entity createInvoiceLine() {
Entity invoiceLine = entities.entity(InvoiceLine.TYPE);
// Set required relationships and values
invoiceLine.set(InvoiceLine.INVOICE_FK, randomInvoice());
invoiceLine.set(InvoiceLine.TRACK_FK, randomTrack());
invoiceLine.set(InvoiceLine.QUANTITY, 1);
return invoiceLine;
}
}
4.3.3. Testing Domain Functions
Test database functions and procedures through the domain:
@Test
void randomPlaylist() {
EntityConnection connection = connection();
connection.startTransaction();
try {
List<Entity> genres = connection.select(limit(Genre.TYPE, 3));
Entity playlist = connection.execute(Playlist.RANDOM_PLAYLIST,
new RandomPlaylistParameters("Test Playlist", 10, genres));
assertNotNull(playlist);
assertEquals(10, connection.count(where(PlaylistTrack.PLAYLIST_FK.equalTo(playlist))));
}
finally {
connection.rollbackTransaction();
}
}
4.4. Model Testing
Test business logic in your Swing models independently of the UI.
4.4.1. Edit Model Testing
public class CountryEditModelTest {
@Test
void averageCityPopulation() {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
CountryEditModel countryEditModel = new CountryEditModel(connectionProvider);
// Load a country
countryEditModel.editor().set(connectionProvider.connection()
.selectSingle(Country.NAME.equalTo("Afghanistan")));
// Test computed values
assertEquals(583_025, countryEditModel.averageCityPopulation().get());
// Test with new entity
countryEditModel.editor().defaults();
assertNull(countryEditModel.averageCityPopulation().get());
}
}
}
4.4.2. Table Model Testing
Test table model behavior and master-detail relationships:
@Test
void albumRefreshedWhenTrackRatingIsUpdated() {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
EntityConnection connection = connectionProvider.connection();
connection.startTransaction();
// Setup test data
Entity album = connection.selectSingle(Album.TITLE.equalTo("Master Of Puppets"));
// Create model and populate
AlbumModel albumModel = new AlbumModel(connectionProvider);
SwingEntityTableModel albumTableModel = albumModel.tableModel();
albumTableModel.queryModel().condition().get(Album.TITLE).set()
.equalTo("Master Of Puppets");
albumTableModel.items().refresh();
// Modify tracks through detail model
List<Entity> tracks = connection.select(Track.ALBUM_FK.equalTo(album));
tracks.forEach(track -> track.set(Track.RATING, 10));
albumModel.detailModels().get(Track.TYPE).editModel().update(tracks);
// Verify album rating was updated
assertEquals(10, albumTableModel.items().visible().get(0).get(Album.RATING));
connection.rollbackTransaction();
}
}
4.5. Integration Testing
Test complete workflows across multiple entities and models.
4.5.1. Testing Report Generation
@Test
void countryReport() throws JRException {
EntityConnectionProvider connectionProvider = createConnectionProvider();
EntityConnection connection = connectionProvider.connection();
CountryReportDataSource dataSource =
new CountryReportDataSource(connection, "North America");
Map<String, Object> parameters = new HashMap<>();
parameters.put("CONTINENT", "North America");
JasperPrint jasperPrint = JasperFillManager.fillReport(
loadReport(), parameters, dataSource);
assertNotNull(jasperPrint);
assertFalse(jasperPrint.getPages().isEmpty());
}
4.6. Test Utilities
4.6.1. Connection Provider Setup
The LocalEntityConnectionProvider.Builder
requires the codion.db.url
system property to be set:
-Dcodion.db.url=jdbc:h2:mem:h2db
Create connection providers for testing:
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
Alternatively, you can provide your own Database
instance:
private static EntityConnectionProvider createConnectionProvider() {
Database database = H2DatabaseFactory.createDatabase("jdbc:h2:mem:testdb");
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.database(database)
.user(UNIT_TEST_USER)
.build();
}
4.6.2. Transaction Management
Always use transactions for data modification tests:
@Test
void updateTest() {
EntityConnection connection = connection();
connection.startTransaction();
try {
// Perform updates
Entity entity = connection.selectSingle(Country.CODE2.equalTo("IS"));
entity.set(Country.POPULATION, 400_000);
connection.update(entity);
// Verify changes
Entity updated = connection.selectSingle(Country.CODE2.equalTo("IS"));
assertEquals(400_000, updated.get(Country.POPULATION));
}
finally {
connection.rollbackTransaction();
}
}
4.6.3. Test Data Builders
Create fluent builders for complex test data:
public class TestDataBuilder {
public static Entity.Builder customer() {
return entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Test")
.with(Customer.LAST_NAME, "Customer")
.with(Customer.EMAIL, "test@example.com");
}
public static Entity.Builder invoice(Entity customer) {
return entities.entity(Invoice.TYPE)
.with(Invoice.CUSTOMER_FK, customer)
.with(Invoice.INVOICE_DATE, LocalDate.now())
.with(Invoice.TOTAL, BigDecimal.ZERO);
}
}
4.7. Testing Best Practices
-
Use DomainTest for all entity types - Even simple entities benefit from the comprehensive validation
-
Test with transactions - Always rollback to keep tests isolated
-
Test observable behavior - Verify that model state changes trigger appropriate notifications
-
Test validation rules - Ensure domain constraints are properly enforced
-
Test computed values - Verify derived attributes and denormalized values update correctly
-
Keep tests focused - Each test should verify one specific behavior
-
Use realistic test data - Your entity factories should create valid, meaningful entities
5. Server
The Codion server provides RMI and HTTP connection options to clients.
5.1. Features
-
Firewall friendly RMI; uses one way communication without callbacks, uses two ports, one for the RMI Registry and one for client connections
-
Integrated web server for serving HTTP client connections, based on Javalin and Jetty
-
All user authentication left to the database by default
-
Comprehensive administration and monitoring facilities via the ServerMonitor
-
Featherweight server with moderate memory and CPU usage
5.2. Security
Here’s a great overview of RMI security risks and mitigations.
5.2.1. Authentication
The Codion server does not perform any user authentication by default, it leaves that up the underlying database. An authentication layer can be added by implementing an Authenticator and registering it with the ServiceLoader.
5.2.2. RMI SSL encryption
To enable SSL encryption between client and server, create a keystore and truststore pair and set the following system properties.
5.2.4. Serialization filtering
The framework provides a way to configure a ObjectInputFilter for deserialization, by specifying a ObjectInputFilterFactory implementation class with the following system property.
codion.server.objectInputFilterFactoryClassName=\
my.serialization.filter.MyObjectInputFilterFactory
Pattern filter
To use the built-in pattern based serialization filter, set the following system property.
codion.server.objectInputFilterFactoryClassName=\
is.codion.common.rmi.server.SerializationFilterFactory
To use serialization filter patterns specified in a string, set the following system property.
codion.server.codion.server.serialization.filter.pattern=pattern1;pattern2
This is equivalent to setting the following:
jdk.serialFilter=pattern1;pattern2
To use the serialization pattern filter based on patterns in a file, set the following system property.
The file may contain all the patterns in a single line, using the ; delimiter or one pattern per line, without a delimiter. Lines starting with '#' are skipped as comments.
codion.server.codion.server.serialization.filter.patternFile=config/patterns.txt
codion.server.serialization.filter.patternFile=classpath:patterns.txt
A list of deserialized classes can be created during a server dry-run by adding the following system property. The file containing all classes deserialized during the run is written to disk on server shutdown.
codion.server.serialization.filter.dryRunFile=deserialized.txt
Example whitelist
ch.qos.logback.classic.Level
com.sun.proxy.$Proxy*
java.lang.Boolean
java.lang.Character
java.lang.Double
java.lang.Enum
java.lang.Float
java.lang.Integer
java.lang.Long
java.lang.Number
java.lang.Object
java.lang.String
java.lang.reflect.Proxy
java.math.BigDecimal
java.math.BigInteger
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.Ser
java.time.ZoneId
java.time.ZoneRegion
java.util.ArrayList
java.util.Arrays$ArrayList
java.util.Collections$EmptyList
java.util.Collections$EmptyMap
java.util.Collections$SingletonList
java.util.Collections$SingletonMap
java.util.Collections$SingletonSet
java.util.Collections$UnmodifiableCollection
java.util.Collections$UnmodifiableList
java.util.Collections$UnmodifiableMap
java.util.Collections$UnmodifiableSet
java.util.Collections$UnmodifiableRandomAccessList
java.util.Date
java.util.HashMap
java.util.HashSet
java.util.LinkedHashMap
java.util.LinkedHashSet
java.util.Locale
java.util.Map$Entry
java.util.UUID
net.sf.jasperreports.compilers.ConstantExpressionEvaluation
net.sf.jasperreports.compilers.FieldEvaluation
net.sf.jasperreports.compilers.ReportExpressionEvaluationData
net.sf.jasperreports.engine.JRPropertiesMap
net.sf.jasperreports.engine.JasperReport
net.sf.jasperreports.engine.JRBand
net.sf.jasperreports.engine.JRExpressionChunk
net.sf.jasperreports.engine.JRField
net.sf.jasperreports.engine.JRParameter
net.sf.jasperreports.engine.JRQueryChunk
net.sf.jasperreports.engine.JRVariable
net.sf.jasperreports.engine.base.JRBaseBand
net.sf.jasperreports.engine.base.JRBaseBoxBottomPen
net.sf.jasperreports.engine.base.JRBaseBoxLeftPen
net.sf.jasperreports.engine.base.JRBaseBoxPen
net.sf.jasperreports.engine.base.JRBaseBoxRightPen
net.sf.jasperreports.engine.base.JRBaseBoxTopPen
net.sf.jasperreports.engine.base.JRBaseDataset
net.sf.jasperreports.engine.base.JRBaseElement
net.sf.jasperreports.engine.base.JRBaseElementGroup
net.sf.jasperreports.engine.base.JRBaseExpression
net.sf.jasperreports.engine.base.JRBaseExpressionChunk
net.sf.jasperreports.engine.base.JRBaseField
net.sf.jasperreports.engine.base.JRBaseLineBox
net.sf.jasperreports.engine.base.JRBaseParagraph
net.sf.jasperreports.engine.base.JRBaseParameter
net.sf.jasperreports.engine.base.JRBasePen
net.sf.jasperreports.engine.base.JRBaseQuery
net.sf.jasperreports.engine.base.JRBaseQueryChunk
net.sf.jasperreports.engine.base.JRBaseReport
net.sf.jasperreports.engine.base.JRBaseSection
net.sf.jasperreports.engine.base.JRBaseStaticText
net.sf.jasperreports.engine.base.JRBaseTextElement
net.sf.jasperreports.engine.base.JRBaseTextField
net.sf.jasperreports.engine.base.JRBaseVariable
net.sf.jasperreports.engine.design.JRReportCompileData
net.sf.jasperreports.engine.type.CalculationEnum
net.sf.jasperreports.engine.type.EvaluationTimeEnum
net.sf.jasperreports.engine.type.HorizontalTextAlignEnum
net.sf.jasperreports.engine.type.IncrementTypeEnum
net.sf.jasperreports.engine.type.OrientationEnum
net.sf.jasperreports.engine.type.PositionTypeEnum
net.sf.jasperreports.engine.type.PrintOrderEnum
net.sf.jasperreports.engine.type.ResetTypeEnum
net.sf.jasperreports.engine.type.RunDirectionEnum
net.sf.jasperreports.engine.type.SectionTypeEnum
net.sf.jasperreports.engine.type.SplitTypeEnum
net.sf.jasperreports.engine.type.StretchTypeEnum
net.sf.jasperreports.engine.type.TextAdjustEnum
net.sf.jasperreports.engine.type.WhenResourceMissingTypeEnum
is.codion.common.Conjunction
is.codion.common.Operator
is.codion.common.db.operation.DefaultFunctionType
is.codion.common.db.operation.DefaultProcedureType
is.codion.common.db.report.AbstractReport
is.codion.common.db.report.DefaultReportType
is.codion.common.rmi.client.DefaultConnectionRequest
is.codion.common.user.DefaultUser
is.codion.common.version.DefaultVersion
is.codion.framework.db.DefaultSelect
is.codion.framework.db.DefaultUpdate
is.codion.framework.domain.DefaultDomainType
is.codion.framework.domain.entity.attribute.DefaultAttribute
is.codion.framework.domain.entity.attribute.DefaultAttribute$DefaultType
is.codion.framework.domain.entity.attribute.DefaultColumn
is.codion.framework.domain.entity.attribute.DefaultForeignKey
is.codion.framework.domain.entity.attribute.DefaultForeignKey$DefaultReference
is.codion.framework.domain.entity.condition.AbstractCondition
is.codion.framework.domain.entity.condition.AbstractColumnCondition
is.codion.framework.domain.entity.condition.DefaultAllCondition
is.codion.framework.domain.entity.condition.DefaultConditionCombination
is.codion.framework.domain.entity.condition.DefaultCustomCondition
is.codion.framework.domain.entity.condition.AbstractColumnCondition
is.codion.framework.domain.entity.condition.DualValueColumnCondition
is.codion.framework.domain.entity.condition.MultiValueColumnCondition
is.codion.framework.domain.entity.condition.SingleValueColumnCondition
is.codion.framework.domain.entity.condition.DefaultConditionType
is.codion.framework.domain.entity.DefaultEntity
is.codion.framework.domain.entity.DefaultEntity$EntityInvoker
is.codion.framework.domain.entity.DefaultEntityType
is.codion.framework.domain.entity.DefaultForeignKey
is.codion.framework.domain.entity.DefaultForeignKey$DefaultReference
is.codion.framework.domain.entity.DefaultKey
is.codion.framework.domain.entity.DefaultOrderBy
is.codion.framework.domain.entity.DefaultOrderBy$DefaultOrderByColumn
is.codion.framework.domain.entity.Entity
is.codion.framework.domain.entity.ImmutableEntity
is.codion.framework.domain.entity.OrderBy$NullOrder
is.codion.demos.chinook.domain.Chinook
is.codion.demos.chinook.domain.Chinook$Invoice
is.codion.demos.chinook.domain.Chinook$Playlist$RandomPlaylistParameters
is.codion.demos.chinook.domain.Chinook$Track
is.codion.demos.chinook.domain.Chinook$Track$RaisePriceParameters
is.codion.demos.employees.domain.*
is.codion.demos.world.domain.api.*
is.codion.demos.petclinic.domain.*
is.codion.plugin.jasperreports.DefaultJRReportType
5.3. Configuration
5.3.1. Example configuration file
# Database configuration
codion.db.url=jdbc:h2:mem:h2db
codion.db.useOptimisticLocking=true
codion.db.countQueries=true
codion.db.initScripts=\
../config/employees/create_schema.sql,\
../config/chinook/create_schema.sql,\
../config/petstore/create_schema.sql,\
../config/world/create_schema.sql
# The admin user credentials, used by the server monitor application
codion.server.admin.user=scott:tiger
# Client logging disabled by default
codion.server.clientLogging=false
# A connection pool based on this user is created on startup
codion.server.connectionPoolUsers=scott:tiger
# The port used by clients
codion.server.port=2222
# The port for the admin interface, used by the server monitor
codion.server.admin.port=4444
# RMI Registry port
codion.server.registryPort=1099
# Any auxiliary servers to run alongside this server
codion.server.auxiliaryServerFactoryClassNames=\
is.codion.framework.servlet.EntityServiceFactory
# The http port
codion.server.http.port=8080
# Specifies whether or not to use https
codion.server.http.secure=false
# The ObjectInputFilterFactory class to use
codion.server.objectInputFilterFactoryClassName=\
is.codion.common.rmi.server.SerializationFilterFactory
# The serialization pattern file to use for RMI deserialization filtering
codion.server.serialization.filter.patternFile=\
../config/serialization-whitelist.txt
# RMI configuration
java.rmi.server.hostname=localhost
java.rmi.server.randomIDs=true
# SSL configuration
javax.net.ssl.keyStore=../config/keystore.jks
javax.net.ssl.keyStorePassword=crappypass
# Used to connect to the server to shut it down
#codion.client.trustStore=../config/truststore.jks
5.4. Code examples
Absolute bare-bones examples of how to run the EntityServer and connect to it.
5.4.1. RMI
Database database = H2DatabaseFactory
.createDatabase("jdbc:h2:mem:testdb",
"src/main/sql/create_schema.sql");
EntityServerConfiguration configuration =
EntityServerConfiguration.builder(SERVER_PORT, REGISTRY_PORT)
.domainClassNames(List.of(Store.class.getName()))
.database(database)
.sslEnabled(false)
.build();
EntityServer server = EntityServer.startServer(configuration);
RemoteEntityConnectionProvider connectionProvider =
RemoteEntityConnectionProvider.builder()
.port(SERVER_PORT)
.registryPort(REGISTRY_PORT)
.domainType(Store.DOMAIN)
.user(parse("scott:tiger"))
.clientType("ClientServer")
.build();
EntityConnection connection = connectionProvider.connection();
List<Entity> customers = connection.select(all(Customer.TYPE));
customers.forEach(System.out::println);
connection.close();
server.shutdown();
5.4.2. HTTP
Database database = H2DatabaseFactory
.createDatabase("jdbc:h2:mem:testdb",
"src/main/sql/create_schema.sql");
EntityService.HTTP_SERVER_PORT.set(HTTP_PORT);
EntityServerConfiguration configuration =
EntityServerConfiguration.builder(SERVER_PORT, REGISTRY_PORT)
.domainClassNames(List.of(Store.class.getName()))
.database(database)
.sslEnabled(false)
.auxiliaryServerFactoryClassNames(List.of(EntityServiceFactory.class.getName()))
.build();
EntityServer server = EntityServer.startServer(configuration);
HttpEntityConnectionProvider connectionProvider =
HttpEntityConnectionProvider.builder()
.port(HTTP_PORT)
.https(false)
.domainType(Store.DOMAIN)
.user(parse("scott:tiger"))
.clientType("ClientServer")
.build();
EntityConnection connection = connectionProvider.connection();
List<Entity> customers = connection.select(all(Customer.TYPE));
customers.forEach(System.out::println);
connection.close();
server.shutdown();
6. Server Monitor
The Codion Server Monitor provides a way to monitor the Codion server.
Below are screenshots of the different server monitor tabs, after ~1 1/2 hours of running the Chinook load test, with ~10 minutes of ramping up to 100 client instances. The server is running on a Raspberry Pi 4, Ubuntu Server 20.10, JDK 19, -Xmx256m, using a HikariCP connection pool on top of an H2 in-memory database.
7. Code style and design
7.1. Factories and builders
Most concrete framework classes, which implement a public interface, are final, package private, and are instantiated with the help of static methods in the interface they implement.
7.1.1. Factories
Static factory methods are provided for classes with a simple state. These are usually named after the interface, which makes using static imports quite convenient.
Event<String> event = event(); // Event.event()
Value<Integer> value = Value.nullable();
State state = State.state(true);
EntityTableConditionModel conditionModel =
entityTableConditionModel(Customer.TYPE, connectionProvider);
7.1.2. Builders
For classes with a more complex state, a builder
method is provided in the interface.
TaskScheduler scheduler =
TaskScheduler.builder(() -> {})
.interval(5, TimeUnit.SECONDS)
.initialDelay(15)
.build();
TemporalField<LocalDate> field =
TemporalField.builder(LocalDate.class)
.dateTimePattern("dd.MM.yyyy")
.columns(12)
.border(createTitledBorder("Date"))
.build();
7.2. Accessors
Immutable fields are accessed using methods named after the field, without a get
/is
prefix.
Observer<String> observer = event.observer();
LocalEntityConnection connection = connectionProvider.connection();
boolean modified = entity.modified();
Entity.Key primaryKey = entity.primaryKey();
A get
/is
prefix implies that the field is mutable and that a corresponding setter method exists, with a set
prefix.
boolean optimisticLocking = connection.isOptimisticLocking();
connection.setOptimisticLocking(false);
7.2.1. Observables
Many classes expose their internal state via the Value class, which can be used to mutate the associated state or observe it by adding listeners or consumers.
FilterListSelection<List<String>> selection = tableModel.selection();
List<Integer> selectedIndexes = selection.indexes().get();
selection.indexes().set(List.of(0, 1, 2));
selection.items().addListener(() -> System.out.println("Selected items changed"));
table.sortingEnabled().set(false);
7.3. Exceptions
There are of course some exceptions to these rules, such as a get
prefix on an accessor for a functionally immutable field or a is
prefix on an immutable boolean field, but these exceptions are usually to keep the style of a class being extended, such as Swing components and should be few and far between.
Value<Integer> integer = Value.nullable();
boolean isNull = integer.isNull();
boolean isNullable = integer.isNullable();
ValueList<Integer> integers = ValueList.valueList();
boolean isEmpty = integers.isEmpty();
8. Modules
8.1. Common
Common classes used throughout the framework.
codion-common-core
Dependency graph
codion-common-db
JDBC related classes.
Dependency graph
codion-common-model
Common model classes.
Dependency graph
codion-common-i18n
Dependency graph
codion-common-rmi
RMI related classes.
Dependency graph
8.2. DBMS
Database specific implementation classes.
codion-dbms-db2
Dependency graph
codion-dbms-derby
Dependency graph
codion-dbms-h2
Dependency graph
codion-dbms-hsqldb
Dependency graph
codion-dbms-mariadb
Dependency graph
codion-dbms-mysql
Dependency graph
codion-dbms-oracle
Dependency graph
codion-dbms-postgresql
Dependency graph
codion-dbms-sqlite
Dependency graph
codion-dbms-sqlserver
Dependency graph
8.3. Framework
The framework itself.
codion-framework-domain
Domain model related classes.
Dependency graph
codion-framework-domain-db
Domain model generation from a database schema.
Dependency graph
codion-framework-domain-test
Domain model unit test related classes.
Dependency graph
codion-framework-db-core
Core database connection related classes.
Dependency graph
codion-framework-db-local
Local JDBC connection related classes.
Dependency graph
codion-framework-db-rmi
RMI connection related classes.
Dependency graph
codion-framework-db-http
HTTP connection related classes.
Dependency graph
codion-framework-i18n
Internationalization strings.
Dependency graph
codion-framework-json-domain
Dependency graph
codion-framework-json-db
Dependency graph
codion-framework-model
Common framework model classes.
Dependency graph
codion-framework-model-test
General application model unit test related classes.
Dependency graph
codion-framework-server
Framework server classes.
Dependency graph
codion-framework-servlet
HTTP servlet server classes.
Dependency graph
8.4. Swing
Swing client implementation.
codion-swing-common-model
Common Swing model classes.
Dependency graph
codion-swing-common-ui
Common Swing UI classes.
Dependency graph
codion-swing-framework-model
Dependency graph
codion-swing-framework-ui
Dependency graph
8.5. Tools
codion-tools-server-monitor-model
Dependency graph
codion-tools-server-monitor-ui
Dependency graph
8.6. Plugins
8.6.1. Logging
codion-plugin-jul-proxy
Dependency graph
codion-plugin-log4j-proxy
Dependency graph
codion-plugin-logback-proxy
Dependency graph
8.6.2. Connection pools
codion-plugin-hikari-pool
Dependency graph
codion-plugin-tomcat-pool
Dependency graph
8.6.4. Look & Feel
Provides all available Flat Look & Feels.
codion-plugin-flatlaf
Dependency graph
codion-plugin-flatlaf-intellij-themes
Provides a bunch of IntelliJ Theme based Flat Look & Feels.
Dependency graph
9. Utilities
9.1. IntelliJ IDEA
9.1.1. Live templates
Here are a few live templates for IntelliJ, reducing the typing required when defining a domain model.
Add this file to the templates directory in the IntelliJ IDEA configuration directory.
View template file
<templateSet group="codion">
<template name="cod" value="Column<Double> $ATTRIBUTE_NAME$ = TYPE.doubleColumn("$COLUMN_NAME$");$END$" description="Column<Double>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="coi" value="Column<Integer> $ATTRIBUTE_NAME$ = TYPE.integerColumn("$COLUMN_NAME$");$END$" description="Column<Integer>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="col" value="Column<Long> $ATTRIBUTE_NAME$ = TYPE.longColumn("$COLUMN_NAME$");$END$" description="Column<Long>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="cos" value="Column<String> $ATTRIBUTE_NAME$ = TYPE.stringColumn("$COLUMN_NAME$");$END$" description="Column<String>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="fk" value="ForeignKey $ATTRIBUTE_NAME$ = TYPE.foreignKey("$FK_NAME$", $END$);" description="ForeignKey" toReformat="false" toShortenFQNames="true">
<variable name="FK_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", FK_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="cold" value="Column<LocalDate> $ATTRIBUTE_NAME$ = TYPE.localDateColumn("$COLUMN_NAME$");$END$" description="Column<LocalDate>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="coldt" value="Column<LocalDateTime> $ATTRIBUTE_NAME$ = TYPE.localDateTimeColumn("$COLUMN_NAME$");$END$" description="Column<LocalDateTime>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="et" value="EntityType TYPE = DOMAIN.entityType("$TABLE_NAME$");$END$" description="EntityType" toReformat="false" toShortenFQNames="true">
<variable name="TABLE_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="cob" value="Column<Boolean> $ATTRIBUTE_NAME$ = TYPE.booleanColumn("$COLUMN_NAME$");$END$" description="Column<Boolean>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="cosh" value="Column<Short> $ATTRIBUTE_NAME$ = TYPE.shortColumn("$COLUMN_NAME$");$END$" description="Column<Short>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="coc" value="Column<Character> $ATTRIBUTE_NAME$ = TYPE.characterColumn("$COLUMN_NAME$");$END$" description="Column<Character>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="coodt" value="Column<OffsetDateTime> $ATTRIBUTE_NAME$ = TYPE.offsetDateTimeColumn("$COLUMN_NAME$");$END$" description="Column<OffsetDateTime>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<template name="coby" value="Column<byte[]> $ATTRIBUTE_NAME$ = TYPE.byteArrayColumn("$COLUMN_NAME$");$END$" description="Column<byte[]>" toReformat="false" toShortenFQNames="true">
<variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
<variable name="ATTRIBUTE_NAME" expression="groovyScript("_1.toUpperCase()", COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
</templateSet>
Available templates
Name | Template |
---|---|
et |
EntityType TYPE = DOMAIN.entityType("table_name"); |
fk |
ForeignKey FK_KEY = TYPE.foreignKey("fk_key"); |
cosh |
Column<Short> COLUMN = TYPE.shortColumn("column"); |
coi |
Column<Integer> COLUMN = TYPE.integerColumn("column"); |
col |
Column<Long> COLUMN = TYPE.longColumn("column"); |
cod |
Column<Double> COLUMN = TYPE.doubleColumn("column"); |
cos |
Column<String> COLUMN = TYPE.stringColumn("column"); |
cold |
Column<LocalDate> COLUMN = TYPE.localDateColumn("column"); |
coldt |
Column<LocalDateTime> COLUMN = TYPE.localDateTimeColumn("column"); |
coodt |
Column<OffsetDateTime> COLUMN = TYPE.offsetDateTimeColumn("column"); |
cob |
Column<Boolean> COLUMN = TYPE.booleanColumn("column"); |
coc |
Column<Character> COLUMN = TYPE.characterColumn("column"); |
coby |
Column<byte[]> COLUMN = TYPE.byteArrayColumn("column"); |
10. Internationalization (i18n)
10.1. Overriding
The default i18n strings can be overridden by implementing Resources and registering the implementation with the ServiceLoader.
package is.codion.demos.chinook.i18n;
import is.codion.common.resource.Resources;
import is.codion.framework.i18n.FrameworkMessages;
import java.util.Locale;
/**
* Replace the english modified warning text and title.
*/
public final class ChinookResources implements Resources {
private static final String FRAMEWORK_MESSAGES =
FrameworkMessages.class.getName();
private final boolean english = Locale.getDefault()
.equals(new Locale("en", "EN"));
@Override
public String getString(String baseBundleName, String key, String defaultString) {
if (english && baseBundleName.equals(FRAMEWORK_MESSAGES)) {
return switch (key) {
case "modified_warning" -> "Unsaved changes will be lost, continue?";
case "modified_warning_title" -> "Unsaved changes";
default -> defaultString;
};
}
return defaultString;
}
}
module is.codion.demos.chinook {
...
provides is.codion.common.resource.Resources
with is.codion.demos.chinook.domain.impl.ChinookResources;
}
See Chinook demo.
10.2. i18n Property Values
For a complete reference of all available i18n property keys and their default values, see i18n Property Values Reference.
11. Architecture Deep Dives
This section provides detailed technical exploration of Codion’s core architectural components and patterns.
11.1. Architecture Deep Dive: Observable Pattern
11.1.1. Overview
The Observable pattern is the foundation of Codion’s reactive architecture. It provides a unified approach to change notification throughout the framework, from UI components to domain models. The pattern consists of four core abstractions:
-
Observer - Manages listeners and consumers for change notifications
-
Observable - Combines a value accessor with change observation
-
Value - Mutable observable wrapper for any object
-
State - Specialized boolean observable with null-to-false coercion
-
Event - Push-only notification mechanism
11.1.2. Core Architecture
Observer Interface
The Observer interface is the foundation for all change notification:
public interface Observer<T> {
// Strong references - prevent garbage collection
boolean addListener(Runnable listener);
boolean addConsumer(Consumer<? super T> consumer);
// Weak references - allow garbage collection
boolean addWeakListener(Runnable listener);
boolean addWeakConsumer(Consumer<? super T> consumer);
// Removal methods
boolean removeListener(Runnable listener);
boolean removeConsumer(Consumer<? super T> consumer);
}
Key design decisions:
-
Two notification types:
-
Runnable
listeners for simple notifications -
Consumer<T>
for data propagation
-
-
Weak reference support: Prevents memory leaks in long-lived UI components
Observable Interface
Observable combines value access with observation:
public interface Observable<T> extends Observer<T> {
@Nullable T get();
default T getOrThrow() {
T value = get();
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
Observer<T> observer(); // For read-only access
}
This separation allows exposing read-only observables while keeping mutation control private.
11.1.3. Value Implementation
Value Interface
Value is the primary mutable observable:
public interface Value<T> extends Observable<T> {
enum Notify {
SET, // Notify on every set() call
CHANGED // Notify only when value changes
}
void set(@Nullable T value);
void clear();
void map(UnaryOperator<T> mapper);
// Linking support
void link(Value<T> originalValue); // Bidirectional
void link(Observable<T> observable); // Unidirectional
// Validation
boolean addValidator(Validator<? super T> validator);
}
Nullable vs Non-Null Values
Codion provides two value types:
// Nullable - can hold null
Value<String> nullable = Value.nullable();
nullable.set(null); // OK
// Non-null - uses null substitute
Value<String> nonNull = Value.nonNull("default");
nonNull.set(null); // Sets to "default"
nonNull.isNull(); // Always false
Value Linking
Values can be linked for automatic synchronization:
Value<Integer> primary = Value.value(10);
Value<Integer> secondary = Value.value(0);
// Bidirectional link
secondary.link(primary); // secondary becomes 10
primary.set(20); // both become 20
secondary.set(30); // both become 30
// Unidirectional link
Value<String> display = Value.value("");
Observable<String> source = getDataSource();
display.link(source); // display follows source changes
11.1.4. State Implementation
State is optimized for boolean values:
public interface State extends ObservableState, Value<Boolean> {
@NonNull Boolean get(); // Never null
ObservableState observable(); // Read-only view
}
State Negation
The not()
method creates an inverse view of a state:
State enabled = State.state(true);
ObservableState disabled = enabled.not();
enabled.get(); // true
disabled.get(); // false
enabled.set(false);
disabled.get(); // true
// Common UI patterns
State processing = State.state();
JButton button = Components.button()
.enabled(processing.not()) // Disabled while processing
.build();
// Combining with other states
State.Combination canEdit = State.and(
loggedIn,
processing.not(),
hasPermission
);
State Combinations
States can be combined using boolean logic:
State canSave = State.state();
State hasChanges = State.state();
State isValid = State.state();
// AND combination
State.Combination saveEnabled = State.and(canSave, hasChanges, isValid);
// OR combination
State.Combination anyProgress = State.or(loading, saving, validating);
// Dynamic combination
State.Combination dynamic = State.combination(Conjunction.AND);
dynamic.add(condition1);
dynamic.add(condition2);
dynamic.remove(condition1);
State Groups
State groups implement radio-button behavior:
State.Group viewMode = State.group();
State listView = State.state();
State tableView = State.state();
State treeView = State.state();
viewMode.add(listView, tableView, treeView);
tableView.set(true); // Others become false
listView.set(true); // tableView becomes false
11.1.5. Event Implementation
Event provides push-only notifications:
public interface Event<T> extends Runnable, Consumer<T>, Observer<T> {
void run(); // Trigger without data
void accept(@Nullable T data); // Trigger with data
Observer<T> observer(); // Read-only access
}
Usage patterns:
// Simple event
Event<Void> refreshRequested = Event.event();
refreshRequested.addListener(this::refresh);
refreshRequested.run();
// Data event
Event<String> errorOccurred = Event.event();
errorOccurred.addConsumer(this::showError);
errorOccurred.accept("Connection failed");
// Both listeners and consumers are notified
Event<Integer> progress = Event.event();
progress.addListener(() -> updateProgressBar());
progress.addConsumer(percent -> setProgress(percent));
progress.accept(75); // Both are called
11.1.6. Thread Safety
The observable implementations use synchronization for thread safety:
-
Listener Management: Adding/removing listeners is synchronized
-
Value Updates: Setting values is atomic
-
Notification Order: Listeners are notified in registration order
-
Exception Handling: Unhandled exceptions in listeners prevent further notifications
Example from DefaultEvent:
private void notifyListeners() {
synchronized (listeners) {
for (Runnable listener : listeners) {
listener.run(); // Exception here stops the loop
}
}
}
Important: If a listener throws an exception, subsequent listeners will not be notified. Always handle exceptions within your listeners:
event.addListener(() -> {
try {
riskyOperation();
} catch (Exception e) {
LOG.error("Error in listener", e);
}
});
11.1.7. Memory Management
Weak References
Weak listeners/consumers prevent memory leaks:
public class DetailPanel {
private final State visible = State.state();
public void attachToMaster(Observable<Entity> selection) {
// Weak reference prevents this panel from keeping
// the selection model alive if panel is discarded
selection.addWeakConsumer(this::showDetails);
}
}
11.1.8. Performance Characteristics
Notification Strategies
Choose the appropriate notification strategy:
// CHANGED: Only when value changes (default)
Value<Integer> counter = Value.builder()
.nonNull(0)
.build(); // Uses CHANGED by default
counter.set(1); // Notifies
counter.set(1); // No notification
// SET: Always notify, even if value unchanged
Value<String> status = Value.builder()
.nonNull("")
.notify(Notify.SET)
.build();
status.set("OK"); // Notifies
status.set("OK"); // Still notifies with SET
11.1.9. Best Practices
-
Use appropriate abstraction:
-
State
for booleans -
Value
for mutable observables -
Event
for actions -
Observable
for read-only exposure
-
-
Prefer weak references for UI components to prevent memory leaks
-
Use validators for domain constraints:
Value<Integer> age = Value.builder() .nonNull(0) .validator(a -> a >= 0 && a <= 150) .build();
-
Link values instead of manual synchronization:
// Instead of: source.addConsumer(value -> target.set(value)); // Use: target.link(source);
-
Expose read-only views:
public class Model { private final State processing = State.state(); public ObservableState processing() { return processing.observable(); } }
11.1.10. Integration Examples
UI Component Binding
// Swing component binding
JTextField textField = new JTextField();
Value<String> model = Value.value("");
// Bidirectional binding
textField.getDocument().addDocumentListener(new DocumentAdapter() {
protected void documentChanged() {
model.set(textField.getText());
}
});
model.addConsumer(textField::setText);
Model State Management
public class EntityEditModel {
private final State modified = State.state();
private final State valid = State.state();
private final State.Combination canSave = State.and(modified, valid);
private final Value<Entity> entity = Value.value();
public EntityEditModel() {
entity.addConsumer(e -> validateEntity());
}
public ObservableState canSave() {
return canSave;
}
}
Event-Driven Architecture
public class Application {
private final Event<Void> shutdownRequested = Event.event();
private final Event<Exception> errorOccurred = Event.event();
public void initialize() {
shutdownRequested.addListener(this::performShutdown);
errorOccurred.addConsumer(this::logError);
errorOccurred.addConsumer(this::notifyUser);
}
}
11.1.11. Summary
Codion’s Observable pattern provides:
-
Unified change notification across the framework
-
Type-safe value observation with validation
-
Memory-safe weak references for UI components
-
Thread-safe implementation for concurrent access
-
Composable state management for complex UI logic
This pattern is fundamental to Codion’s reactive architecture, enabling automatic UI updates, clean separation of concerns, and maintainable application state management.