The EntityEditor is the framework’s write path: it manages a single entity instance being edited — its values, their validity, dirty state and default values — and performs the insert, update and delete operations. An editor is available from every edit model via editor(), and everything an EntityEditPanel does — component enabling, validation indicators, dirty warnings — it does by observing the editor.
The editor exposes two things: the entity being edited, via entity(), and an observable value for each attribute, via value(attribute).
1. The entity
EditorEntity represents the entity being edited: set() populates the editor, defaults() initializes a new entity with default values, clear() empties it, revert() reverts all modifications. Its exists(), modified() and valid() observable states drive the UI — an insert control is enabled while the entity does not exist, an update control while it exists and is modified and valid.
The example below shows the full life cycle: defaults, setting values, insert, modify, update and delete.
EntityConnectionProvider connectionProvider =
EntityConnectionProvider.builder()
.domain(Store.DOMAIN)
.user(User.parse("scott:tiger"))
.clientType("StoreMisc")
.build();
CustomerEditModel editModel = new CustomerEditModel(connectionProvider);
SwingEntityEditor editor = editModel.editor();
editor.value(Customer.ID).defaultValue()
.set(() -> UUID.randomUUID().toString());
//sets the defaults
editor.entity().defaults();
//set the values
editor.value(Customer.FIRST_NAME).set("Björn");
editor.value(Customer.LAST_NAME).set("Sigurðsson");
editor.value(Customer.ACTIVE).set(true);
//inserts and returns the inserted entity
Entity customer = editor.insert();
//modify some values
editor.value(Customer.FIRST_NAME).set("John");
editor.value(Customer.LAST_NAME).set("Doe");
//updates and returns the updated entity
customer = editor.update();
//deletes the active entity
editor.delete();
2. Editing values
EditorValue is a full Value implementation for a single attribute, so anything that can be linked to a Value — an input component, another value — can be linked to an attribute of the entity being edited. Each editor value also exposes the state the UI needs: valid() and modified() observable states, a validation message(), the original() value and revert().
Two observers notify of changes, with an important distinction:
-
Value.observer() — notified whenever the value changes, whether by the user or by the framework populating the editor.
-
edited() — notified only when the value is changed through this EditorValue, that is, by an actual edit, not when the entity is set or cleared.
Use edited() to react to user edits without also reacting every time an entity is selected into the editor:
// We populate the unit price when the track is edited
Observer<Entity> trackEdited = editor().value(InvoiceLine.TRACK_FK).edited();
trackEdited.when(Objects::nonNull)
.addConsumer(this::setUnitPrice);
trackEdited.when(Objects::isNull)
.addListener(this::clearUnitPrice);
3. Default values and persistent values
Each editor value has a defaultValue() supplier, used by entity().defaults() when initializing a new entity. A default value can be configured in the domain model, via the attribute definition, or set on the editor value directly, as in the UUID example in the previous section.
The persist() state controls whether a value survives defaults() — whether it carries over from one entity to the next. Foreign key values persist by default, since when entering a batch of records the reference typically stays the same, while the other values change.
4. Foreign key values: persist and propagate
The example below disables persistence for a foreign key and uses propagate() to populate the invoice billing address from the customer, each time the customer is edited. A propagated value is applied only when the source value actually changes and remains editable by the user afterwards.
public final class InvoiceEditModel extends SwingEntityEditModel {
public InvoiceEditModel(EntityConnectionProvider connectionProvider) {
super(Invoice.TYPE, connectionProvider);
EditorValue<Entity> customer = editor().value(Invoice.CUSTOMER_FK);
// By default, foreign key values persist when the model
// is cleared, here we disable that for CUSTOMER_FK
customer.persist().set(false);
// We populate the invoice address fields with
// the customer address when the customer is edited
customer.propagate(Invoice.BILLINGADDRESS, cust -> valueOrNull(cust, Customer.ADDRESS));
customer.propagate(Invoice.BILLINGCITY, cust -> valueOrNull(cust, Customer.CITY));
customer.propagate(Invoice.BILLINGPOSTALCODE, cust -> valueOrNull(cust, Customer.POSTALCODE));
customer.propagate(Invoice.BILLINGSTATE, cust -> valueOrNull(cust, Customer.STATE));
customer.propagate(Invoice.BILLINGCOUNTRY, cust -> valueOrNull(cust, Customer.COUNTRY));
}
private static @Nullable <T> T valueOrNull(Entity customer, Attribute<T> attribute) {
return customer == null ? null : customer.get(attribute);
}
}
5. Inserting, updating and deleting
insert(), update() and delete() operate on the entity being edited and return the resulting entity (or entities, for the collection variants). The entity is validated before insert and update, using the validator(), and an invalid entity fails with an EntityValidationException — the same validation continuously reflected by the valid() states and message() of each editor value.
|
Note
|
These methods perform the operation on the calling thread. UI code, such as EntityEditPanel, uses tasks() to prepare an operation for background execution, keeping the UI responsive. |
6. Custom persistence
EntityPersistence, set via persistence(), replaces how the editor performs its insert, update and delete — without changing anything else about it. Here invoice line operations run in a transaction which also updates the totals of the affected invoices, via a database procedure:
private static final class InvoiceLinePersistence implements EntityPersistence {
@Override
public Collection<Entity> insert(Collection<Entity> invoiceLines, EntityConnection connection) {
// Use a transaction to update the invoice totals when an invoice line is inserted
return transaction(connection, () -> updateTotals(connection.insertSelect(invoiceLines), connection));
}
@Override
public Collection<Entity> update(Collection<Entity> invoiceLines, EntityConnection connection) {
// Use a transaction to update the invoice totals when an invoice line is updated
return transaction(connection, () -> updateTotals(connection.updateSelect(invoiceLines), connection));
}
@Override
public void delete(Collection<Entity> invoiceLines, EntityConnection connection) {
// Use a transaction to update the invoice totals when an invoice line is deleted
transaction(connection, () -> {
connection.delete(primaryKeys(invoiceLines));
updateTotals(invoiceLines, connection);
});
}
private static Collection<Entity> updateTotals(Collection<Entity> invoiceLines, EntityConnection connection) {
// Get the IDs of the invoices that need their totals updated
Collection<Long> invoiceIds = distinct(InvoiceLine.INVOICE_ID, invoiceLines);
// Execute the UPDATE_TOTALS procedure
connection.execute(Invoice.UPDATE_TOTALS, invoiceIds);
return invoiceLines;
}
}
7. Detail editors
An editor can edit related entities alongside its own, via detail() and EditorLink. A detail editor is populated when the master entity is set, and its entity is inserted, updated or deleted along with the master, in the same transaction. The link’s present predicate decides whether the detail entity should exist at all — a detail entity failing the predicate is deleted rather than saved.
Here customer preferences are edited alongside the customer:
public final class CustomerEditModel extends SwingEntityEditModel {
public CustomerEditModel(EntityConnectionProvider connectionProvider) {
super(Customer.TYPE, connectionProvider);
editor().comboBoxModels().initialize(Customer.SUPPORTREP_FK);
// Set a detail editor, in order to edit customer preferences alongside the customer
SwingEntityEditor preferences = new SwingEntityEditor(Preferences.TYPE, connectionProvider);
preferences.value(Preferences.PREFERRED_GENRE_FK).persist().set(false);
preferences.comboBoxModels().initialize(Preferences.PREFERRED_GENRE_FK);
editor().detail().add(EditorLink.builder()
.editor(preferences)
.foreignKey(Preferences.CUSTOMER_FK)
.select(customer -> where(Preferences.CUSTOMER_FK.equalTo(customer))
.referenceDepth(Preferences.CUSTOMER_FK, 0)
.build())
.present(new PreferencesPresent())
.build());
}
private static final class PreferencesPresent implements Predicate<Entity> {
@Override
public boolean test(Entity preferences) {
// Preferences without both preferred genre and newsletter are deleted
return preferences.present(Preferences.PREFERRED_GENRE_FK) ||
preferences.present(Preferences.NEWSLETTER);
}
}
}
The link’s select provides the query fetching the detail entity for a given master, which the artist editor below uses to edit a fixed number of tag entities, one link per tag slot:
public final class ArtistEditModel extends SwingEntityEditModel {
public static final int TAG_SLOTS = 6;
public static final String TAG_PREFIX = "tag";
public ArtistEditModel(EntityConnectionProvider connectionProvider) {
super(Artist.TYPE, connectionProvider);
TagPresent present = new TagPresent();
for (int i = 0; i < TAG_SLOTS; i++) {
editor().detail().add(EditorLink.builder()
.editor(new SwingEntityEditor(ArtistTag.TYPE, connectionProvider))
.foreignKey(ArtistTag.ARTIST_FK)
.select(new TagSelect(i))
.present(present)
.name(TAG_PREFIX + i)
.caption(String.valueOf(i + 1))
.build());
}
}
private static final class TagSelect implements DetailSelect {
private final int index;
private TagSelect(int index) {
this.index = index;
}
@Override
public Select get(Entity artist) {
return Select.where(ArtistTag.ARTIST_FK.equalTo(artist))
.orderBy(ascending(ArtistTag.TAG))
.referenceDepth(0)
.offset(index)
.limit(1)
.build();
}
}
private static final class TagPresent implements Predicate<Entity> {
@Override
public boolean test(Entity tag) {
return tag.present(ArtistTag.TAG);
}
}
}
8. Events
events() provides application-wide insert, update and delete notifications for the edited entity type. These are what keep the rest of a running application consistent without any wiring: combo box models refresh or reconcile, search model selections receive updated instances, table models react to insert and delete, and foreign key values in other editors are replaced when the entity they reference is updated or deleted elsewhere. Reach for these events when application logic must react to persistence outcomes regardless of which editor performed the operation.