A Note taking application in ~300 lines.

notes demo


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 DefaultDomain {

    private Notes() {

  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
      addBeforeUpdateListener(notes ->
              notes.values().forEach(note ->
                      note.put(Note.UPDATED, LocalDateTime.now())));

  private static final class NoteEditPanel extends EntityEditPanel  {

    private NoteEditPanel(NoteEditModel editModel) {
      // CLEAR is the only standard control we require, for clearing the UI
      super(editModel, EditControl.CLEAR);

    protected void initializeUI() {

              .hintText("Take note...")
              // Use the Enter key for inserting, updating
              // and deleting, depending on the edit model state

      add(component(Note.NOTE).get(), BorderLayout.CENTER);
      // Add a button based on the CLEAR control
      add(new JButton(control(EditControl.CLEAR).get()), BorderLayout.EAST);

    private void insertDeleteOrUpdate() {
      if (editModel().exists().not().get() && editModel().isNotNull(Note.NOTE).get()) {
        // A new note with a non-empty text
      else if (editModel().modified().get()) {
        if (editModel().isNull(Note.NOTE).get()) {
          // An existing note with empty text
        else {
          // An existing note with a modified text

  private static final class NoteTableModel extends SwingEntityTableModel {

    private NoteTableModel(EntityConnectionProvider connectionProvider) {
      super(new NoteEditModel(connectionProvider));
      //configure the table model and columns
      sortModel().setSortOrder(Note.CREATED, SortOrder.DESCENDING);

  private static final class NoteTablePanel extends EntityTablePanel  {

    private NoteTablePanel(NoteTableModel tableModel) {
      // 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.

  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) {
              new NoteEditPanel(noteModel.editModel()),
              new NoteTablePanel(noteModel.tableModel()));
      // No need to include the default control buttons since
      // we added the CLEAR control button to the edit panel

    protected JPanel createEditBasePanel(EntityEditPanel editPanel) {
      // Override to replace the default panel which uses a FlowLayout, in order
      // to have the edit panel fill the horizontal width of the parent panel
      return Components.borderLayoutPanel()

  public static final class NotesApplicationModel extends SwingEntityApplicationModel {

    private static final Version VERSION = Version.version(1, 0);

    public NotesApplicationModel(EntityConnectionProvider connectionProvider) {
      super(connectionProvider, VERSION);
      NoteModel noteModel = new NoteModel(connectionProvider);
      // Refresh the table model to populate it

  public static final class NotesApplicationPanel extends EntityApplicationPanel<NotesApplicationModel> {

    public NotesApplicationPanel(NotesApplicationModel applicationModel) {
      super(applicationModel, applicationPanel -> {
        // Override the default JTabbedPane based layout,
        // since we're only displaying a single panel
        NotePanel notePanel = applicationPanel.entityPanel(Note.TYPE);
        notePanel.initialize();// Lazy initialization of UI components
        applicationPanel.add(notePanel, BorderLayout.CENTER);
        applicationPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));

    protected List<EntityPanel> createEntityPanels() {
      NoteModel noteModel = applicationModel().entityModel(Note.TYPE);

      return singletonList(new NotePanel(noteModel));

  private static final class NotesConnectionProviderFactory implements ConnectionProviderFactory {

    public EntityConnectionProvider createConnectionProvider(User user,
                                                             DomainType domainType,
                                                             String clientTypeId,
                                                             Version clientVersion) {
      Database database = new H2DatabaseFactory()

      // Here we create the EntityConnectionProvider instance
      // manually so we can safely ignore some method parameters
      return LocalEntityConnectionProvider.builder()
              // Create the schema
              // 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())

  private static void startApplication() throws Exception {
    // Change the default horizontal alignment for temporal table columns
    // Make all the IntelliJ themes from Flat Look and Feel available (View -> Select Look & Feel)

    EntityApplicationPanel.builder(NotesApplicationModel.class, NotesApplicationPanel.class)
            .frameSize(new Dimension(600, 500))
            // No need for a startup dialog since startup is very quick
            // Supply our connection provider factory from above
            .connectionProviderFactory(new NotesConnectionProviderFactory())
            // Automatically login with the H2Database super user
            // Runs on the EventDispatchThread

  public static void main(String[] args) throws Exception {

  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')",
    try (Connection connection = database.createConnection(User.user("sa"));
         Statement stmt = connection.createStatement()) {
      for (String statement : CREATE_SCHEMA_STATEMENTS) {
      for (String statement : dataStatements) {

      return database;
    catch (Exception e) {
      throw new RuntimeException(e);