A Note taking application in ~300 lines.
gradlew codion-demos-manual:runNotesDemo
demos/manual/src/main/java/is/codion/framework/demos/manual/notes/NotesDemo.java
public final class NotesDemo {
private static final List<String> CREATE_SCHEMA_STATEMENTS = asList(
"create schema notes",
"create table notes.note(" +
"id identity not null, " +
"note text not null, " +
"created timestamp default now() not null, " +
"updated timestamp)"
);
private static final DomainType DOMAIN = DomainType.domainType("notes");
// The domain model API.
interface Note {
EntityType TYPE = DOMAIN.entityType("notes.note");
Column<Long> ID = TYPE.longColumn("id");
Column<String> NOTE = TYPE.stringColumn("note");
Column<LocalDateTime> CREATED = TYPE.localDateTimeColumn("created");
Column<LocalDateTime> UPDATED = TYPE.localDateTimeColumn("updated");
}
// The domain model implementation
private static class Notes extends DomainModel {
private Notes() {
super(DOMAIN);
add(Note.TYPE.define(
Note.ID.define()
.primaryKey(),
Note.NOTE.define()
.column()
.caption("Note")
.nullable(false),
Note.CREATED.define()
.column()
.caption("Created")
.nullable(false)
.updatable(false)
.columnHasDefaultValue(true),
Note.UPDATED.define()
.column()
.caption("Updated"))
.keyGenerator(KeyGenerator.identity())
.orderBy(OrderBy.descending(Note.CREATED))
.caption("Notes")
.build());
}
}
private static final class NoteEditModel extends SwingEntityEditModel {
private NoteEditModel(EntityConnectionProvider connectionProvider) {
super(Note.TYPE, connectionProvider);
// Set the Note.UPDATED value before we perform an update
beforeUpdate().addConsumer(notes ->
notes.values().forEach(note ->
note.put(Note.UPDATED, LocalDateTime.now())));
}
}
private static final class NoteEditPanel extends EntityEditPanel {
private NoteEditPanel(NoteEditModel editModel) {
super(editModel);
// CLEAR is the only standard control we require, for clearing the UI
configureControls(config -> config.clear()
.control(CLEAR));
}
@Override
protected void initializeUI() {
initialFocusAttribute().set(Note.NOTE);
createTextField(Note.NOTE)
.hint("Take note...")
// Use the Enter key for inserting, updating
// and deleting, depending on the edit model state
.action(Control.command(this::insertDeleteOrUpdate));
setLayout(Layouts.borderLayout());
add(component(Note.NOTE).get(), BorderLayout.CENTER);
// Add a button based on the CLEAR control
add(new JButton(control(CLEAR).get()), BorderLayout.EAST);
}
private void insertDeleteOrUpdate() {
EditableEntity entity = editModel().entity();
if (entity.exists().not().get() && entity.isNotNull(Note.NOTE).get()) {
// A new note with a non-empty text
insert();
}
else if (entity.modified().get()) {
if (entity.isNull(Note.NOTE).get()) {
// An existing note with empty text
deleteWithConfirmation();
}
else {
// An existing note with a modified text
updateWithConfirmation();
}
}
}
}
private static final class NoteTableModel extends SwingEntityTableModel {
private NoteTableModel(EntityConnectionProvider connectionProvider) {
super(new NoteEditModel(connectionProvider));
onInsert().set(OnInsert.ADD_TOP_SORTED);
}
}
private static final class NoteTablePanel extends EntityTablePanel {
private NoteTablePanel(NoteTableModel tableModel) {
super(tableModel, config -> config
// Exclude the Note.UPDATED attribute from the Edit popup menu since
// the value is set automatically and shouldn't be editable via the UI.
// Note.CREATED is excluded by default since it is not updatable.
.editable(attributes -> attributes.remove(Note.UPDATED)));
// Configure the table and columns
table().model().sorter().setSortOrder(Note.CREATED, SortOrder.DESCENDING);
table().setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
FilterTableColumnModel<Attribute<?>> columnModel = table().columnModel();
columnModel.column(Note.NOTE).setPreferredWidth(280);
columnModel.column(Note.CREATED).setPreferredWidth(130);
columnModel.column(Note.UPDATED).setPreferredWidth(130);
}
}
private static final class NoteModel extends SwingEntityModel {
private NoteModel(EntityConnectionProvider connectionProvider) {
super(new NoteTableModel(connectionProvider));
}
}
private static final class NotePanel extends EntityPanel {
private NotePanel(NoteModel noteModel) {
super(noteModel,
new NoteEditPanel(noteModel.editModel()),
new NoteTablePanel(noteModel.tableModel()), config -> config
// No need to include the default control buttons since
// we added the CLEAR control button to the edit panel
.includeControls(false)
// Replace the default edit base panel which uses a FlowLayout, in order
// to have the edit panel fill the horizontal width of the parent panel
.editBasePanel(editPanel -> Components.borderLayoutPanel()
.centerComponent(editPanel)
.build()));
}
}
public static final class NotesApplicationModel extends SwingEntityApplicationModel {
private static final Version VERSION = Version.builder().major(1).build();
public NotesApplicationModel(EntityConnectionProvider connectionProvider) {
super(connectionProvider, VERSION);
NoteModel noteModel = new NoteModel(connectionProvider);
addEntityModel(noteModel);
// Refresh the table model to populate it
noteModel.tableModel().refresh();
}
}
public static final class NotesApplicationPanel extends EntityApplicationPanel<NotesApplicationModel> {
public NotesApplicationPanel(NotesApplicationModel applicationModel) {
super(applicationModel, NotesApplicationLayout::new);
}
@Override
protected List<EntityPanel> createEntityPanels() {
NoteModel noteModel = applicationModel().entityModel(Note.TYPE);
return List.of(new NotePanel(noteModel));
}
// Replace the default JTabbedPane based layout,
// since we're only displaying a single panel
private static final class NotesApplicationLayout implements ApplicationLayout {
private final EntityApplicationPanel<?> applicationPanel;
private NotesApplicationLayout(EntityApplicationPanel<?> applicationPanel) {
this.applicationPanel = applicationPanel;
}
@Override
public JComponent layout() {
return Components.borderLayoutPanel()
// initialize() must be called to initialize the UI components
.centerComponent(applicationPanel.entityPanel(Note.TYPE).initialize())
.border(BorderFactory.createEmptyBorder(5, 5, 0, 5))
.build();
}
}
}
private static final class NotesConnectionProviderFactory implements ConnectionProviderFactory {
@Override
public EntityConnectionProvider create(User user,
DomainType domainType,
String clientTypeId,
Version clientVersion) {
Database database = new H2DatabaseFactory()
.create("jdbc:h2:mem:h2db;DB_CLOSE_DELAY=-1");
// Here we create the EntityConnectionProvider instance
// manually so we can safely ignore some method parameters
return LocalEntityConnectionProvider.builder()
// Create the schema
.database(createSchema(database))
// Supply a domain model instance, if we used the domainType we'd
// need to register the domain model in the ServiceLoader in order for
// the framework to instantiate one for us
.domain(new Notes())
.clientVersion(clientVersion)
.user(user)
.build();
}
}
private static void startApplication() throws Exception {
// Change the default horizontal alignment for temporal table columns
FilterTableCellRenderer.TEMPORAL_HORIZONTAL_ALIGNMENT.set(SwingConstants.CENTER);
// Make all the IntelliJ themes from Flat Look and Feel available (View -> Select Look & Feel)
Arrays.stream(FlatAllIJThemes.INFOS)
.forEach(LookAndFeelProvider::addLookAndFeel);
EntityApplicationPanel.builder(NotesApplicationModel.class, NotesApplicationPanel.class)
.frameTitle("Notes")
.frameSize(new Dimension(600, 500))
.applicationVersion(NotesApplicationModel.VERSION)
// No need for a startup dialog since startup is very quick
.displayStartupDialog(false)
// Supply our connection provider factory from above
.connectionProviderFactory(new NotesConnectionProviderFactory())
// Automatically login with the H2Database super user
.automaticLoginUser(User.user("sa"))
.defaultLookAndFeelClassName(FlatMaterialDarkerIJTheme.class.getName())
// Runs on the EventDispatchThread
.start();
}
public static void main(String[] args) throws Exception {
startApplication();
}
private static Database createSchema(Database database) {
List<String> dataStatements = asList(
"insert into notes.note(note, created) " +
"values ('My first note', '2023-10-03 10:40')",
"insert into notes.note(note, created) " +
"values ('My second note', '2023-10-03 12:20')",
"insert into notes.note(note, created, updated) " +
"values ('My third note', '2023-10-04 08:50', '2023-10-04 08:52')",
"insert into notes.note(note, created) " +
"values ('My fourth note', '2023-10-05 09:03')",
"insert into notes.note(note, created) " +
"values ('My fifth note', '2023-10-05 18:30')",
"commit"
);
try (Connection connection = database.createConnection(User.user("sa"));
Statement stmt = connection.createStatement()) {
for (String statement : CREATE_SCHEMA_STATEMENTS) {
stmt.execute(statement);
}
for (String statement : dataStatements) {
stmt.execute(statement);
}
return database;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}