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.

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: 4

otherValue.set(3);

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

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

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

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 object can be linked to a State instance via the Utilities.linkToEnabledState method, where the action’s enabled status is updated according to the state.

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

The core JDBC related classes.

2.1. Common classes

Two common classes used throughout the framework are the Database and DatabaseConnection classes.

2.1.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);

2.1.2. DatabaseConnection

The DatabaseConnection class represents a connection to a database instance and is a wrapper around a java.sql.Connection instance and provides transaction control.

A DatabaseConnection instance is created via the databaseConnection factory methods.

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

Database database = Database.instance();

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

DatabaseConnection databaseConnection =
        DatabaseConnection.databaseConnection(database, user);

databaseConnection.startTransaction();
try {
  java.sql.Connection connection = databaseConnection.getConnection();
  connection.createStatement().execute("select 1");
  databaseConnection.commitTransaction();
}
catch (SQLException e) {
  databaseConnection.rollbackTransaction();
  throw new DatabaseException(e);
}
catch (Exception e) {
  databaseConnection.rollbackTransaction();
  throw new RuntimeException(e);
}

databaseConnection.close();

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

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
  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 VisibleItems instance to replace the edited
  // row since the row objects are records and thereby immutable
  private final VisibleItems<Person> items;

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

  @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>> supplier = () -> 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())
                .supplier(supplier)
                .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();

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

// Create the columns, specifying the identifier and the model index
List<FilterTableColumn<String>> columns = List.of(
        FilterTableColumn.builder()
                .identifier(Person.NAME)
                .modelIndex(0)
                .build(),
        FilterTableColumn.builder()
                .identifier(Person.AGE)
                .modelIndex(1)
                .build());

FilterTable<Person, String> table =
        FilterTable.builder()
                .model(tableModel)
                .columns(columns)
                .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);
Export
String tabDelimited = table.export()
        // Tab delimited
        .delimiter('\t')
        // Include hidden columns
        .hidden(true)
        // Include header
        .header(true)
        // Only selected rows
        .selected(true)
        .get();

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

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<Integer, NumberField<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)
                .valueRange(0, 10_000)
                .groupingUsed(false)
                .build();
Long
Value<Long> longValue = Value.nullable();

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

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

NumberField<BigDecimal> bigDecimalField =
        Components.bigDecimalField()
                .link(bigDecimalValue)
                .maximumFractionDigits(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 =
        (NullableCheckBox) Components.checkBox()
                .link(booleanValue)
                .text("Check")
                .nullable(true)
                .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().visible().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<Person, PersonPanel> {

  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 -> notifyListeners());
    component.lastNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyListeners());
  }

  @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. 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.4.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.4.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.4.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.4.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. Model