1. Common Core

1.1. Core classes

Three common classes used throughout the framework are Event, State and Value and their respective observers Observer and ObservableState.

Note
Not all available methods are included in the diagrams below, see javadocs for details.

1.1.1. Event

event diagram

The Event class is a synchronous event implementation used throughout the framework. Classes typically expose observers for their events via public accessors. Events are triggered by calling the run method in case no data is associated with the event or accept in case data should be propogated to consumers.

The associated Observer instance can not trigger the event and can be safely passed around.

Event listeners must implement either Runnable or Consumer, depending on whether they are interested in the data associated with the event.

Note
Both listeners and consumer are notified each time the event is triggered, regardless of whether run or accept is used, listeners and consumers are notified in the order they were added.

Events are instantiated via factory methods in the Event class.

// specify an event propagating
// a String as the event data
Event<String> event = Event.event();

// an observer manages the listeners
// for an Event but can not trigger it
Observer<String> observer = event.observer();

// add a listener if you're not
// interested in the event data
observer.addListener(() -> System.out.println("Event occurred"));

event.run();//output: 'Event occurred'

// or a consumer if you're
// interested in the event data
observer.addConsumer(data -> System.out.println("Event: " + data));

event.accept("info");//output: 'Event: info'

// Event implements Observer so
// listeneres can be added directly without
// referring to the Observer
event.addConsumer(System.out::println);

1.1.2. Value

value diagram

A Value wraps a value and provides a change observer.

Values are instantiated via factory methods in the Value class.

Values can be linked so that changes in one are reflected in the other.

// a nullable value with 2 as the initial value
Value<Integer> value =
        Value.nullable(2);

value.set(4);

// a non-null value using 0 as null substitute
Value<Integer> otherValue =
        Value.nonNull(0);

// linked to the value above
value.link(otherValue);

System.out.println(otherValue.get());// output: 0

otherValue.set(3);

System.out.println(value.get());// output: 3

System.out.println(value.is(3));// output: true

value.set(null);

System.out.println(otherValue.get());// output: 0

value.addConsumer(System.out::println);

otherValue.addListener(() ->
        System.out.println("Value changed: " + otherValue.get()));

Values can be non-nullable if a nullValue is specified when the value is initialized. Null is then translated to the nullValue when set.

Integer initialValue = 42;
Integer nullValue = 0;

Value<Integer> value =
        Value.builder()
                .nonNull(nullValue)
                .value(initialValue)
                .build();

System.out.println(value.isNullable());//output: false

System.out.println(value.get());// output: 42

value.set(null); //or value.clear();

value.isNull(); //output: false;

System.out.println(value.get());//output: 0
ValueCollection
ValueSet<Integer> valueSet =
        ValueSet.<Integer>builder()
                .value(Set.of(1, 2, 3))
                .build();

valueSet.addListener(() -> System.out.println("Values changed"));

valueSet.add(4); //output: Values changed
valueSet.add(1); //no change, no output

valueSet.remove(1); //output: Values changed
System.out.println(valueSet.contains(1)); //output: false

valueSet.clear();

ValueList<Integer> valueList =
        ValueList.<Integer>builder()
                .value(List.of(1, 2, 3))
                .build();

valueList.addListener(() -> System.out.println("Values changed"));

valueList.add(4); //output: Values changed
valueList.add(1); //output: Values changed

valueList.remove(1); //output: Values changed
System.out.println(valueList.contains(1)); //output: true

valueList.clear();

1.1.3. State

state diagram

The State class encapsulates a boolean state and provides read only access and a change observer via ObservableState. A State implements Value<Boolean> and is non-nullable with null translating to false.

States are instantiated via factory methods in the State class.

// a boolean state, false by default
State state = State.state();

// an observable manages the listeners for a State but can not modify it
ObservableState observable = state.observable();
// a not observable is always available, which is
// always the reverse of the original state
ObservableState not = state.not();

// add a listener notified each time the state changes
observable.addListener(() -> System.out.println("State changed"));

state.set(true);//output: 'State changed'

observable.addConsumer(value -> System.out.println("State: " + value));

state.set(false);//output: 'State: false'

// State extends ObservableState so listeners can be added
// directly without referring to the ObservableState
state.addListener(() -> System.out.println("State changed"));
private static final class IntegerValue {

  private final State negative = State.state(false);
  private final Value<Integer> integer = Value.builder()
          .nonNull(0)
          .consumer(value -> negative.set(value < 0))
          .build();

  /**
   * Increment the value by one
   */
  public void increment() {
    integer.map(value -> value + 1);
  }

  /**
   * Decrement the value by one
   */
  public void decrement() {
    integer.map(value -> value - 1);
  }

  /**
   * @return an observer notified each time the value changes
   */
  public Observer<Integer> changed() {
    return integer.observable();
  }

  /**
   * @return a state observer indicating whether the value is negative
   */
  public ObservableState negative() {
    return negative.observable();
  }
}

Any Action or JComponent enabled status can be linked to a State instance via the Utilities.enabled() method.

State composition
State updateEnabled = State.state();
State insertEnabled = State.state();

State recordNew = State.state();
State recordModified = State.state();

ObservableState saveButtonEnabled = State.and(
        State.or(insertEnabled, updateEnabled),
        State.or(recordNew, recordModified));

JButton saveButton = new JButton("Save");

Utilities.enabled(saveButtonEnabled, saveButton);
State state = State.state();

Action action = new AbstractAction("action") {
  @Override
  public void actionPerformed(ActionEvent e) {
    System.out.println("Hello Action");
  }
};

Utilities.enabled(state, action);

System.out.println(action.isEnabled());// output: false

state.set(true);

System.out.println(action.isEnabled());// output: true

Controls can also be linked to a State instance.

State state = State.state();

CommandControl control = Control.builder()
        .command(() -> System.out.println("Hello Control"))
        .enabled(state)
        .build();

System.out.println(control.isEnabled());// output: false

state.set(true);

System.out.println(control.isEnabled());// output: true
Note
When a State or Event is linked to a Swing component, for example its enabled state, all state changes must happen on the Event Dispatch Thread.

2. Common Database

Core JDBC related classes.

2.1. Database

The Database class represents a DBMS instance and provides connections to that instance.

There are multiple ways to aquire a Database instance.

  • By specifying a JDBC url via a system property.

System.setProperty("codion.db.url", "jdbc:h2:mem:h2db");

Database database = Database.instance();
  • By setting the JDBC url configuration value directly (which also sets the system property).

Database.URL.set("jdbc:h2:mem:h2db");

Database database = Database.instance();
  • By instantiating a DatabaseFactory directly.

String url = "jdbc:h2:mem:h2db";

DatabaseFactory databaseFactory = DatabaseFactory.instance(url);

Database database = databaseFactory.create(url);
  • By instantiating a DBMS specific DatabaseFactory directly.

String url = "jdbc:h2:mem:h2db";

H2DatabaseFactory databaseFactory = new H2DatabaseFactory();

Database database = databaseFactory.create(url);

A Database instance provides java.sql.Connection instances via the createConnection method.

Database.URL.set("jdbc:h2:mem:h2db");

Database database = Database.instance();

User user = User.parse("scott:tiger");

java.sql.Connection connection = database.createConnection(user);

3. Common Model

3.1. File Preferences

UserPreferences.file(String) provides a file-based implementation of the Java Preferences API that removes the restrictive length limitations of the default implementation.

3.1.1. Motivation

The default Java Preferences API imposes the following restrictions:

  • Maximum key length: 80 characters

  • Maximum value length: 8,192 characters (8 KB)

  • Maximum node name length: 80 characters

These limits can be problematic when storing configuration data such as serialized table column preferences or other structured data that may exceed these limits.

3.1.2. Usage

// Then use preferences normally
Preferences prefs = UserPreferences.file("my.config.file");
prefs.put("my.very.long.key.name.that.exceeds.80.chars", "my huge value...");
prefs.flush(); // Writes to ~/.codion/my.config.file.json

3.1.3. File Storage

Preferences are stored in a JSON file at a platform-specific location:

  • Windows: %LOCALAPPDATA%\Codion{filename}.json

  • macOS: ~/Library/Preferences/Codion/{filename}.json

  • Linux: ~/.config/codion/{filename}.json (follows XDG Base Directory specification)

  • Other: ~/.codion/{filename}.json

Note
The global preferences file location can also be specified via UserPreferences.PREFERENCES_LOCATION if these default locations do not fit your use-case.

The file uses the following JSON format:

{
  "normal.key": "normal value",
  "very.long.key.that.exceeds.eighty.characters": "value",
  "key.with.large.value": "... 100KB of text ...",
  "key.with.newlines": "Line 1\nLine 2\nLine 3"
}
Note
When storing JSON data as a preference value (such as serialized column preferences), the JSON content is properly escaped and stored as a JSON string value. This double-encoding is handled automatically - you store and retrieve your JSON strings normally through the Preferences API.

3.1.4. Features

  • No length restrictions - Keys and values can be of any length

  • JSON format - Human-readable and easily editable

  • Thread-safe - Safe for concurrent access within a single JVM

  • Multi-JVM safe - File locking ensures safe concurrent access from multiple JVMs

  • Atomic writes - Changes are written atomically to prevent corruption

  • Drop-in replacement - Uses the standard Java Preferences API

  • Full hierarchy support - Create nested preference nodes with paths

3.1.5. Hierarchy Support

The file preferences implementation supports the full Java Preferences node hierarchy:

Preferences root = UserPreferences.file("my.config.file");

// Create nested preference nodes
Preferences appNode = root.node("myapp");
Preferences uiNode = appNode.node("ui");
Preferences dbNode = appNode.node("database");

// Store preferences at different levels
uiNode.put("theme", "dark");
uiNode.put("font.size", "14");
dbNode.put("connection.url", "jdbc:postgresql://localhost/mydb");
dbNode.put("connection.pool.size", "10");

// Navigate to nodes using paths
Preferences ui = root.node("myapp/ui");
String theme = ui.get("theme", "light"); // "dark"

// List child nodes
String[] appChildren = appNode.childrenNames(); // ["ui", "database"]

// Remove entire node and its children
dbNode.removeNode();
root.flush();

The hierarchical structure is stored as nested JSON objects:

{
  "myapp": {
    "ui": {
      "theme": "dark",
      "font.size": "14"
    },
    "database": {
      "connection.url": "jdbc:postgresql://localhost/mydb",
      "connection.pool.size": "10"
    }
  }
}

3.1.6. Concurrency and Multi-JVM Access

The file preferences implementation is designed to be safe for concurrent access:

  • Within a single JVM: All operations are synchronized using internal locks

  • Across multiple JVMs: File locking ensures only one JVM can write at a time

  • Atomic writes: Changes are written to a temporary file and atomically moved

  • External changes: The sync() method reloads the file if modified externally

// JVM 1
Preferences prefs1 = UserPreferences.file("my.config.file");
prefs1.put("shared.value", "from JVM 1");
prefs1.flush();

// JVM 2
Preferences prefs1 = UserPreferences.file("my.config.file");
prefs2.sync(); // Reload to see changes from JVM 1
String value = prefs2.get("shared.value", null); // "from JVM 1"

The implementation uses a 5-second timeout for acquiring file locks to prevent deadlocks.

4. Swing Common Model

4.1. Table Model

4.1.1. FilterTableModel

filter table model diagram

The FilterTableModel is a table model central to the framework.

// Define a record representing the table rows
public record Person(String name, int age) {

  // Constants identifying the table columns,
  // used as column header captions by default.
  public static final String NAME = "Name";
  public static final String AGE = "Age";
}
// Implement TableColumns, which specifies the column identifiers,
// the column class and how to extract column values from row objects
public static final class PersonColumns implements TableColumns<Person, String> {

  private static final List<String> COLUMNS = List.of(NAME, AGE);

  @Override
  public List<String> identifiers() {
    return COLUMNS;
  }

  @Override
  public Class<?> columnClass(String column) {
    return switch (column) {
      case NAME -> String.class;
      case AGE -> Integer.class;
      default -> throw new IllegalArgumentException();
    };
  }

  @Override
  public Object value(Person person, String column) {
    return switch (column) {
      case NAME -> person.name();
      case AGE -> person.age();
      default -> throw new IllegalArgumentException();
    };
  }
}
// Implement a Editor for handling row edits
private static final class PersonEditor implements Editor<Person, String> {

  // We need the underlying IncludedItems instance to replace the edited
  // row since the row objects are records and thereby immutable
  private final IncludedItems<Person> items;

  private PersonEditor(FilterTableModel<Person, String> tableModel) {
    this.items = tableModel.items().included();
  }

  @Override
  public boolean editable(Person person, String identifier) {
    // Both columns editable
    return true;
  }

  @Override
  public void set(Object value, int rowIndex, Person person, String identifier) {
    switch (identifier) {
      case NAME -> items.set(rowIndex, new Person((String) value, person.age()));
      case AGE -> items.set(rowIndex, new Person(person.name(), (Integer) value));
    }
  }
}
// Implement an item supplier responsible for supplying
// the data when the table items are refreshed.
// Without one the model can be populated by adding items manually
Supplier<Collection<Person>> items = () -> List.of(
        new Person("John", 42),
        new Person("Mary", 43),
        new Person("Andy", 33),
        new Person("Joan", 37));

// Build the table model, providing the TableColumns
// implementation along with the item supplier and row editor.
FilterTableModel<Person, String> tableModel =
        FilterTableModel.builder()
                .columns(new PersonColumns())
                .items(items)
                .editor(PersonEditor::new)
                .build();

// Populate the model
tableModel.items().refresh();
Selection
FilterListSelection<Person> selection = tableModel.selection();

// Print the selected items when they change
selection.items().addConsumer(System.out::println);

// Print a message when the minimum selected index changes
selection.index().addListener(() ->
        System.out.println("Selected index changed"));

// Select the first row
selection.index().set(0);

// Select the first two rows
selection.indexes().set(List.of(0, 1));

// Fetch the selected items
List<Person> items = selection.items().get();

// Or just the first (minimum index)
Person item = selection.item().get();

// Select a specific person
selection.item().set(new Person("John", 42));

// Select all persons over 40
selection.items().set(person -> person.age() > 40);

// Increment all selected indexes by
// one, moving the selection down
selection.indexes().increment();

// Clear the selection
selection.clear();
Filters
TableConditionModel<String> filters = tableModel.filters();

// Filter out people under 40 years old
ConditionModel<Integer> ageFilter = filters.get(Person.AGE);

ageFilter.set().greaterThanOrEqualTo(40);
// Not necessary since filters auto-enable by default
// when operators and operands are specified
ageFilter.enabled().set(true);

// Filter is automatically disabled when it is cleared
ageFilter.clear();

// Filter out anyone besides John and Joan
ConditionModel<String> nameFilter = filters.get(NAME);

nameFilter.caseSensitive().set(false);
nameFilter.set().equalTo("jo%");

// Clear all filters
filters.clear();
Sorting
FilterTableSort<Person, String> sort = tableModel.sort();

// Sort by age and name, ascending
sort.ascending(AGE, NAME);

// Sort by age, descending,
// set() clears the previous sort
sort.order(AGE).set(DESCENDING);
// add sorting by name, ascending,
// add() adds to any previous sort
sort.order(NAME).add(ASCENDING);

// Clear the sorting
sort.clear();
Export
String tabDelimited = tableModel.export()
        // Specify columns (default all, so not really necessary here)
        .columns(List.of(Person.NAME, Person.AGE))
        // Tab delimited
        .delimiter('\t')
        // Include header
        .header(true)
        // Only selected rows
        .selected(true)
        .get();

5. Swing Common UI

5.1. Table UI

5.1.1. FilterTable

filter table diagram

The FilterTable is a JTable subclass central to the framework.

// See FilterTableModel example
FilterTableModel<Person, String> tableModel = createFilterTableModel();

FilterTable<Person, String> table =
        FilterTable.builder()
                .model(tableModel)
                .cellRenderer(Person.AGE, FilterTableCellRenderer.builder()
                        .columnClass(Integer.class)
                        .horizontalAlignment(SwingConstants.CENTER)
                        .build())
                .doubleClick(Control.command(() ->
                        tableModel.selection().item().optional()
                                .ifPresent(System.out::println)))
                .autoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS)
                .build();
Columns
FilterTableColumnModel<String> columns = table.columnModel();

// Reorder the columns
columns.visible().set(Person.AGE, Person.NAME);

// Print hidden columns when they change
columns.hidden().addConsumer(System.out::println);

// Hide the age column
columns.visible(Person.AGE).set(false);

// Only show the age column
columns.visible().set(Person.AGE);

// Reset columns to their default location and visibility
columns.reset();
FilterTableSearchModel search = table.search();

// Search for the value "43" in the table
search.predicate().set(value -> value.equals("43"));

RowColumn searchResult = search.results().current().get();

System.out.println(searchResult); // row: 1, column: 1

// Print the next available result
search.results().next().ifPresent(System.out::println);

5.2. Input Controls

5.2.1. Control

control diagram
State somethingEnabledState = State.state(true);

CommandControl control = Control.builder()
        .command(() -> System.out.println("Doing something"))
        .caption("Do something")
        .mnemonic('D')
        .enabled(somethingEnabledState)
        .build();

JButton somethingButton = new JButton(control);

Control.ActionCommand actionCommand = actionEvent -> {
  if ((actionEvent.getModifiers() & ActionEvent.SHIFT_MASK) != 0) {
    System.out.println("Doing something else");
  }
};
CommandControl actionControl = Control.builder()
        .action(actionCommand)
        .caption("Do something else")
        .mnemonic('S')
        .build();

JButton somethingElseButton = new JButton(actionControl);

5.2.2. ToggleControl

State state = State.state();

ToggleControl toggleStateControl = Control.builder()
        .toggle(state)
        .build();

JToggleButton toggleButton = Components.toggleButton()
        .toggle(toggleStateControl)
        .text("Change state")
        .mnemonic('C')
        .build();

Value<Boolean> booleanValue = Value.nonNull(false);

ToggleControl toggleValueControl = Control.builder()
        .toggle(booleanValue)
        .build();

JCheckBox checkBox = Components.checkBox()
        .toggle(toggleValueControl)
        .text("Change value")
        .mnemonic('V')
        .build();

Value<Boolean> nullableBooleanValue = Value.nullable();

ToggleControl nullableToggleControl = Control.builder()
        .toggle(nullableBooleanValue)
        .build();

NullableCheckBox nullableCheckBox = Components.nullableCheckBox()
        .toggle(nullableToggleControl)
        .build();

5.2.3. Controls

Controls controls = Controls.builder()
        .control(Control.builder()
                .command(this::doFirst)
                .caption("First")
                .mnemonic('F'))
        .control(Control.builder()
                .command(this::doSecond)
                .caption("Second")
                .mnemonic('S'))
        .control(Controls.builder()
                .caption("Submenu")
                .control(Control.builder()
                        .command(this::doSubFirst)
                        .caption("Sub-first")
                        .mnemonic('b'))
                .control(Control.builder()
                        .command(this::doSubSecond)
                        .caption("Sub-second")
                        .mnemonic('u')))
        .build();

JMenu menu = Components.menu()
        .controls(controls)
        .build();

Control firstControl = Control.builder()
        .command(this::doFirst)
        .caption("First")
        .mnemonic('F')
        .build();
Control secondControl = Control.builder()
        .command(this::doSecond)
        .caption("Second")
        .mnemonic('S')
        .build();

Controls twoControls = Controls.builder()
        .controls(firstControl, secondControl)
        .build();

JPanel buttonPanel = Components.buttonPanel()
        .controls(twoControls)
        .build();

5.3. Input Components

Binding model data to UI components is accomplished by linking a Value instance to an instance of its subclass ComponentValue, which represents a value based on an input component.

//a nullable integer value, initialized to 42
Value<Integer> integerValue =
        Value.nullable(42);

//create a spinner linked to the value
JSpinner spinner =
        Components.integerSpinner()
                .link(integerValue)
                .build();

//create a NumberField component value, basically doing the same as
//the above, with an extra step to expose the underlying ComponentValue
ComponentValue<NumberField<Integer>, Integer> numberFieldValue =
        Components.integerField()
                //linked to the same value
                .link(integerValue)
                .buildValue();

//fetch the input field from the component value
NumberField<Integer> numberField = numberFieldValue.component();

5.3.1. Text

TextField
Value<String> stringValue = Value.nullable();

JTextField textField =
        Components.stringField()
                .link(stringValue)
                .preferredWidth(120)
                .transferFocusOnEnter(true)
                .build();
Value<Character> characterValue = Value.nullable();

JTextField textField =
        Components.characterField()
                .link(characterValue)
                .preferredWidth(120)
                .transferFocusOnEnter(true)
                .build();
TextArea
Value<String> stringValue = Value.nullable();

JTextArea textArea =
        Components.textArea()
                .link(stringValue)
                .rowsColumns(10, 20)
                .lineWrap(true)
                .build();

5.3.2. Numbers

Integer
Value<Integer> integerValue = Value.nullable();

NumberField<Integer> integerField =
        Components.integerField()
                .link(integerValue)
                .range(0, 10_000)
                .grouping(false)
                .build();
Long
Value<Long> longValue = Value.nullable();

NumberField<Long> longField =
        Components.longField()
                .link(longValue)
                .grouping(true)
                .build();
Double
Value<Double> doubleValue = Value.nullable();

NumberField<Double> doubleField =
        Components.doubleField()
                .link(doubleValue)
                .fractionDigits(3)
                .decimalSeparator('.')
                .build();
BigDecimal
Value<BigDecimal> bigDecimalValue = Value.nullable();

NumberField<BigDecimal> bigDecimalField =
        Components.bigDecimalField()
                .link(bigDecimalValue)
                .fractionDigits(2)
                .groupingSeparator('.')
                .decimalSeparator(',')
                .build();

5.3.3. Date & Time

LocalTime
Value<LocalTime> localTimeValue = Value.nullable();

TemporalField<LocalTime> temporalField =
        Components.localTimeField()
                .link(localTimeValue)
                .dateTimePattern("HH:mm:ss")
                .build();
LocalDate
Value<LocalDate> localDateValue = Value.nullable();

TemporalField<LocalDate> temporalField =
        Components.localDateField()
                .link(localDateValue)
                .dateTimePattern("dd-MM-yyyy")
                .build();
LocalDateTime
Value<LocalDateTime> localDateTimeValue = Value.nullable();

TemporalField<LocalDateTime> temporalField =
        Components.localDateTimeField()
                .link(localDateTimeValue)
                .dateTimePattern("dd-MM-yyyy HH:mm")
                .build();

5.3.4. Boolean

CheckBox
//non-nullable so use this value instead of null
boolean nullValue = false;

Value<Boolean> booleanValue =
        Value.builder()
                .nonNull(nullValue)
                .value(true)
                .build();

JCheckBox checkBox =
        Components.checkBox()
                .link(booleanValue)
                .text("Check")
                .horizontalAlignment(SwingConstants.CENTER)
                .build();
NullableCheckBox
//nullable boolean value
Value<Boolean> booleanValue = Value.nullable();

NullableCheckBox checkBox =
        Components.nullableCheckBox()
                .link(booleanValue)
                .text("Check")
                .build();
ComboBox
Value<Boolean> booleanValue = Value.nullable();

JComboBox<Item<Boolean>> comboBox =
        Components.booleanComboBox()
                .link(booleanValue)
                .toolTipText("Select a value")
                .build();

5.3.5. Selection

ComboBox
Value<String> stringValue = Value.nullable();

DefaultComboBoxModel<String> comboBoxModel =
        new DefaultComboBoxModel<>(new String[] {"one", "two", "three"});

JComboBox<String> comboBox =
        Components.comboBox()
                .model(comboBoxModel)
                .link(stringValue)
                .preferredWidth(160)
                .build();
FilterComboBoxModel
Supplier<Collection<String>> items = () ->
        List.of("One", "Two", "Three");

FilterComboBoxModel<String> model =
        FilterComboBoxModel.builder()
                .items(items)
                .nullItem("-")
                .build();

JComboBox<String> comboBox =
        Components.comboBox()
                .model(model)
                .mouseWheelScrolling(true)
                .build();

// Hides the 'Two' item.
model.items().included().predicate()
        .set(item -> !item.equals("Two"));

// Prints the selected item
model.selection().item()
        .addConsumer(System.out::println);

// Refreshes the items using the supplier from above
model.items().refresh();
Completion

Completion provides a way to enable completion for combo boxes.

The available completion modes are:

Combo boxes created via Components have completion enabled by default, with MAXIMUM_MATCH being the default completion mode.

The default completion mode is controlled via the Completion.COMPLETION_MODE configuration value.

Normalization

Strings are normalized by default during completion, that is, accents are removed, i.e. á, í and ú become a, i and u. To enable accented character sensitivity, normalization can be turned off, either globally via the Completion.NORMALIZE configuration value or individually via the combo box builder.

FilterComboBoxModel<String> model =
        FilterComboBoxModel.builder()
                .items(List.of("Jon", "Jón", "Jónsi"))
                .nullItem("-")
                .build();

JComboBox<String> comboBox =
        Components.comboBox()
                .model(model)
                // Auto completion
                .completionMode(Completion.Mode.AUTOCOMPLETE)
                // Accented characters not normalized
                .normalize(false)
                .build();

5.3.6. Custom

TextField

In the following example we link a value based on a Person class to a component value displaying text fields for a first and last name.

class Person {
  final String firstName;
  final String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return lastName + ", " + firstName;
  }
}

class PersonPanel extends JPanel {
  final JTextField firstNameField = new JTextField();
  final JTextField lastNameField = new JTextField();

  public PersonPanel() {
    setLayout(new GridLayout(2, 2));
    add(new JLabel("First name"));
    add(new JLabel("Last name"));
    add(firstNameField);
    add(lastNameField);
  }
}

class PersonPanelValue extends AbstractComponentValue<PersonPanel, Person> {

  public PersonPanelValue(PersonPanel component) {
    super(component);
    //We must call notifyListeners() each time this value changes,
    //that is, when either the first or last name changes.
    component.firstNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyObserver());
    component.lastNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyObserver());
  }

  @Override
  protected Person getComponentValue() {
    return new Person(component().firstNameField.getText(), component().lastNameField.getText());
  }

  @Override
  protected void setComponentValue(Person value) {
    component().firstNameField.setText(value == null ? null : value.firstName);
    component().lastNameField.setText(value == null ? null : value.lastName);
  }
}

Value<Person> personValue = Value.nullable();

PersonPanel personPanel = new PersonPanel();

Value<Person> personPanelValue = new PersonPanelValue(personPanel);

personPanelValue.link(personValue);

5.3.7. Examples

5.4. Dialogs

5.4.1. Component

Event<?> close = Event.event();

JButton closeButton = Components.button()
        .control(Control.builder()
                .command(close::run)
                .caption("Close"))
        .build();

Dialogs.builder()
        .component(closeButton)
        .owner(window)
        .title("Dialog")
        .disposeOnEscape(false)
        .closeObserver(close)
        .show();

5.4.2. Selection

Items
Dialogs.select()
        .list(List.of("One", "Two", "Three"))
        .owner(window)
        .title("Select a number")
        .select()
        .single()
        .ifPresent(System.out::println);
Collection<String> selected = Dialogs.select()
        .list(List.of("One", "Two", "Three", "Four"))
        .owner(window)
        .title("Select numbers")
        .select()
        .multiple();
Files
File file = Dialogs.select()
        .files()
        .owner(window)
        .title("Select a file")
        .selectFile();
Collection<File> files = Dialogs.select()
        .files()
        .owner(window)
        .title("Select files")
        .filter(new FileNameExtensionFilter("PDF files", "pdf"))
        .selectFiles();
File fileToSave = Dialogs.select()
        .files()
        .owner(window)
        .title("Select file to save")
        .confirmOverwrite(false)
        .selectFileToSave("default-filename.txt");
File directory = Dialogs.select()
        .files()
        .owner(window)
        .title("Select a directory")
        .selectDirectory();
Collection<File> directories = Dialogs.select()
        .files()
        .owner(window)
        .title("Select directories")
        .selectDirectories();
File fileOrDirectory = Dialogs.select()
        .files()
        .owner(window)
        .title("Select file or directory")
        .selectFileOrDirectory();
Collection<File> filesOrDirectories = Dialogs.select()
        .files()
        .owner(window)
        .title("Select files and/or directories")
        .selectFilesOrDirectories();

5.4.3. Action Dialogs

Ok Cancel
Dialogs.okCancel()
        .owner(window)
        .title("Title")
        .onOk(this::onOk)
        .onCancel(this::onCancel)
        .show();
Action
Dialogs.action()
        .owner(window)
        .title("Title")
        .defaultAction(Control.builder()
                .command(this::onOk)
                .caption(Messages.ok())
                .build())
        .escapeAction(Control.builder()
                .command(this::onCancel)
                .caption(Messages.cancel())
                .build())
        .show();

5.4.4. Input

ComponentValue<NumberField<Integer>, Integer> component =
        Components.integerField()
                .value(42)
                .buildValue();

Integer input = Dialogs.input()
        .component(component)
        .owner(window)
        .title("Input")
        .valid(State.present(component))
        .show();

5.4.5. Exception

Dialogs.exception()
        .owner(window)
        .title("Exception")
        .unwrap(List.of(RuntimeException.class))
        // Don't include system properties
        .systemProperties(false)
        .show(exception);

5.4.6. Calendar

Dialogs.calendar()
        .owner(window)
        .title("Calendar")
        .selectLocalDate()
        .ifPresent(System.out::println);

5.4.7. Progress

Progress dialogs provide feedback during long-running operations. To prevent unnecessary visual noise, progress dialogs support configurable delays for both showing and hiding the dialog.

The .delay() method accepts two parameters: the delay before showing the dialog (in milliseconds) and the delay before hiding it. These delays are based on human-computer interaction research:

  • Show delay (350ms default): Operations completing in under ~300ms feel instantaneous to users, so showing a progress indicator would be distracting. A delay of 350ms ensures the dialog only appears for operations that users actually perceive as taking time.

  • Hide delay (800ms default): When an operation completes just after the dialog appears, hiding it immediately would create a jarring flash. The hide delay smooths this transition and reduces the perceived duration of short operations by avoiding the visual disruption of rapidly appearing and disappearing dialogs.

Dialogs.progressWorker()
        .task(this::performTask)
        .owner(window)
        .title("Performing task")
        .delay(500, 1000)
        .onResult(this::handleResult)
        .onException(this::handleException)
        .execute();

5.5. ProgressWorker

ProgressWorker is a SwingWorker extension, providing a fluent API for constructing background task workers for a variety of task types.

All handlers get called on the EventDispatchThread.

Note
Like SwingWorker, ProgressWorker instances can not be reused. Tasks, on the other hand, can be made stateful and reusable if required.

5.5.1. Task

// A non-progress aware task, producing no result
ProgressWorker.Task task = () -> {
  // Perform the task
};

ProgressWorker.builder()
        .task(task)
        .onException(exception ->
                Dialogs.exception()
                        .owner(applicationFrame)
                        .show(exception))
        .execute();

5.5.2. ResultTask

// A non-progress aware task, producing a result
ProgressWorker.ResultTask<String> task = () -> {
  // Perform the task
  return "Result";
};

ProgressWorker.builder()
        .task(task)
        .onResult(result ->
                showMessageDialog(applicationFrame, result))
        .onException(exception ->
                Dialogs.exception()
                        .owner(applicationFrame)
                        .show(exception))
        .execute();

5.5.3. ProgressTask

// A progress aware task, producing no result
ProgressWorker.ProgressTask<String> task = progressReporter -> {
  // Perform the task
  progressReporter.report(42);
  progressReporter.publish("Message");
};

ProgressWorker.builder()
        .task(task)
        .onProgress(progress ->
                System.out.println("Progress: " + progress))
        .onPublish(message ->
                showMessageDialog(applicationFrame, message))
        .onException(exception ->
                Dialogs.exception()
                        .owner(applicationFrame)
                        .show(exception))
        .execute();

5.5.4. ProgressResultTask

// A reusable, cancellable task, producing a result.
// Displays a progress bar in a dialog while running.
var task = new DemoProgressResultTask();

ProgressWorker.builder()
        .task(task.prepare(142))
        .onStarted(task::started)
        .onProgress(task::progress)
        .onPublish(task::publish)
        .onDone(task::done)
        .onCancelled(task::cancelled)
        .onException(task::failed)
        .onResult(task::finished)
        .execute();
static final class DemoProgressResultTask implements ProgressResultTask<Integer, String> {

  private final JProgressBar progressBar = progressBar()
          .indeterminate(false)
          .stringPainted(true)
          .string("")
          .build();
  // Indicates whether the task has been cancelled
  private final AtomicBoolean cancelled = new AtomicBoolean();
  // A Control for setting the cancelled state
  private final Control cancel = Control.builder()
          .command(() -> cancelled.set(true))
          .caption("Cancel")
          .mnemonic('C')
          .build();
  // A panel containing the progress bar and cancel button
  private final JPanel progressPanel = borderLayoutPanel()
          .center(progressBar)
          .east(button()
                  .control(cancel))
          .build();
  // The dialog displaying the progress panel
  private final JDialog dialog = Dialogs.builder()
          .component(progressPanel)
          .owner(applicationFrame)
          // Trigger the cancel control with the Escape key
          .keyEvent(KeyEvents.builder()
                  .keyCode(VK_ESCAPE)
                  .action(cancel))
          // Prevent the dialog from closing on Escape
          .disposeOnEscape(false)
          .build();

  private int taskSize;

  @Override
  public int maximum() {
    return taskSize;
  }

  @Override
  public Integer execute(ProgressReporter<String> progressReporter) throws Exception {
    List<Integer> result = new ArrayList<>();
    for (int i = 0; i < taskSize; i++) {
      Thread.sleep(50);
      if (cancelled.get()) {
        throw new CancelException();
      }
      result.add(i);
      reportProgress(progressReporter, i);
    }

    return result.stream()
            .mapToInt(Integer::intValue)
            .sum();
  }

  // Makes this task reusable by resetting the internal state
  private DemoProgressResultTask prepare(int taskSize) {
    this.taskSize = taskSize;
    progressBar.getModel().setMaximum(taskSize);
    cancelled.set(false);

    return this;
  }

  private void reportProgress(ProgressReporter<String> reporter, int progress) {
    reporter.report(progress);
    if (progress < taskSize * 0.5) {
      reporter.publish("Going strong");
    }
    else if (progress > taskSize * 0.5 && progress < taskSize * 0.85) {
      reporter.publish("Half way there");
    }
    else if (progress > taskSize * 0.85) {
      reporter.publish("Almost done");
    }
  }

  private void started() {
    dialog.setVisible(true);
  }

  private void progress(int progress) {
    progressBar.setValue(progress);
  }

  private void publish(List<String> strings) {
    progressBar.setString(strings.get(0));
  }

  private void done() {
    dialog.setVisible(false);
  }

  private void cancelled() {
    showMessageDialog(applicationFrame, "Cancelled");
  }

  private void failed(Exception exception) {
    Dialogs.exception()
            .owner(applicationFrame)
            .show(exception);
  }

  private void finished(Integer result) {
    showMessageDialog(applicationFrame, "Result : " + result);
  }
}

6. Common Utilities

Codion provides a few classes with miscellanous utility functions.

6.1. Core

6.2. TaskScheduler

TaskScheduler provides a simple, lightweight way to execute tasks periodically on a background thread.

6.2.1. Basic Usage

Create a scheduler that runs a task at fixed intervals:

// Build a scheduler that runs a task every 5 seconds
TaskScheduler scheduler =
        TaskScheduler.builder()
                .task(() -> System.out.println("Running scheduled task"))
                .interval(5, TimeUnit.SECONDS)
                .initialDelay(10) // Wait 10 seconds before first execution
                .name("My Task Scheduler") // Name for debugging
                .build();

// Start the scheduler
scheduler.start();

// Check if it's running
boolean running = scheduler.running();

// Stop the scheduler when done
scheduler.stop();

6.2.2. Auto-Start

Use the start() method to build and start the scheduler in one step:

// Build and start in one step
TaskScheduler scheduler =
        TaskScheduler.builder()
                .task(this::performMaintenance)
                .interval(30, TimeUnit.SECONDS)
                .name("Maintenance Task")
                .start(); // Builds and starts immediately

6.2.3. Thread Naming

The name() method sets the thread name, which is useful for debugging and thread dumps:

TaskScheduler scheduler =
    TaskScheduler.builder()
        .task(maintenanceTask)
        .interval(30, TimeUnit.SECONDS)
        .name("Connection Maintenance") // Appears in thread dumps
        .start();
Tip
Always name your scheduler threads descriptively to make debugging easier.

6.2.4. Custom ThreadFactory

For advanced control over thread creation, provide a custom ThreadFactory:

// Use a custom ThreadFactory for advanced control
TaskScheduler scheduler =
        TaskScheduler.builder()
                .task(() -> System.out.println("Custom thread task"))
                .interval(1, TimeUnit.MINUTES)
                .threadFactory(runnable -> {
                  Thread thread = new Thread(runnable);
                  thread.setDaemon(true);
                  thread.setPriority(Thread.MIN_PRIORITY);
                  thread.setName("Custom Task Thread");
                  return thread;
                })
                .start();
Note
When using a custom threadFactory(), the name() setting is ignored since the ThreadFactory has full control over thread creation.

6.2.5. Common Use Cases

TaskScheduler is ideal for:

  • Connection maintenance - Periodically cleaning up idle connections

  • Cache cleanup - Removing stale cache entries

  • Statistics collection - Gathering metrics at regular intervals

  • Health checks - Monitoring system health

  • Periodic saves - Auto-saving user preferences or state

All scheduler threads are daemon threads by default, so they won’t prevent JVM shutdown.