1. Domain

package org.jminor.framework.demos.petclinic.domain;

public interface Clinic {

  String T_VET = "petclinic.vet";
  String VET_ID = "id";
  String VET_FIRST_NAME = "first_name";
  String VET_LAST_NAME = "last_name";

  String T_SPECIALTY = "petclinic.specialty";
  String SPECIALTY_ID = "id";
  String SPECIALTY_NAME = "name";

  String T_VET_SPECIALTY = "petclinic.vet_specialty";
  String VET_SPECIALTY_VET = "vet";
  String VET_SPECIALTY_VET_FK = "vet_fk";
  String VET_SPECIALTY_SPECIALTY = "specialty";
  String VET_SPECIALTY_SPECIALTY_FK = "specialty_fk";

  String T_PET_TYPE = "petclinic.pet_type";
  String PET_TYPE_ID = "id";
  String PET_TYPE_NAME = "name";

  String T_OWNER = "petclinic.owner";
  String OWNER_ID = "id";
  String OWNER_FIRST_NAME = "first_name";
  String OWNER_LAST_NAME = "last_name";
  String OWNER_ADDRESS = "address";
  String OWNER_CITY = "city";
  String OWNER_TELEPHONE = "telephone";

  String T_PET = "petclinic.pet";
  String PET_ID = "id";
  String PET_NAME = "name";
  String PET_BIRTH_DATE = "birth_date";
  String PET_PET_TYPE_ID = "type_id";
  String PET_PET_TYPE_FK = "type_fk";
  String PET_OWNER_ID = "owner_id";
  String PET_OWNER_FK = "owner_fk";

  String T_VISIT = "petclinic.visit";
  String VISIT_ID = "id";
  String VISIT_PET_ID = "pet_id";
  String VISIT_PET_FK = "pet_fk";
  String VISIT_DATE = "date";
  String VISIT_DESCRIPTION = "description";
}
package org.jminor.framework.demos.petclinic.domain.impl;

import org.jminor.framework.domain.Domain;
import org.jminor.framework.domain.entity.StringProvider;

import java.sql.Types;

import static org.jminor.framework.demos.petclinic.domain.Clinic.*;
import static org.jminor.framework.domain.entity.KeyGenerators.automatic;
import static org.jminor.framework.domain.entity.OrderBy.orderBy;
import static org.jminor.framework.domain.property.Properties.*;

public final class ClinicImpl extends Domain {

  public ClinicImpl() {
    vet();
    specialty();
    vetSpecialty();
    petType();
    owner();
    pet();
    visit();
  }

  private void vet() {
    define(T_VET,
            primaryKeyProperty(VET_ID),
            columnProperty(VET_FIRST_NAME, Types.VARCHAR, "First name")
                    .maximumLength(30)
                    .nullable(false),
            columnProperty(VET_LAST_NAME, Types.VARCHAR, "Last name")
                    .maximumLength(30)
                    .nullable(false))
            .keyGenerator(automatic(T_VET))
            .caption("Vets")
            .searchPropertyIds(VET_FIRST_NAME, VET_LAST_NAME)
            .stringProvider(new StringProvider(VET_LAST_NAME)
                    .addText(", ").addValue(VET_FIRST_NAME))
            .orderBy(orderBy().ascending(VET_LAST_NAME, VET_FIRST_NAME))
            .smallDataset(true);
  }

  private void specialty() {
    define(T_SPECIALTY,
            primaryKeyProperty(SPECIALTY_ID),
            columnProperty(SPECIALTY_NAME, Types.VARCHAR, "Name")
                    .maximumLength(80)
                    .nullable(false))
            .keyGenerator(automatic(T_SPECIALTY))
            .caption("Specialties")
            .searchPropertyIds(SPECIALTY_NAME)
            .stringProvider(new StringProvider(SPECIALTY_NAME))
            .smallDataset(true);
  }

  private void vetSpecialty() {
    define(T_VET_SPECIALTY,
            foreignKeyProperty(VET_SPECIALTY_VET_FK, "Vet", T_VET,
                    columnProperty(VET_SPECIALTY_VET)
                            .primaryKeyIndex(0))
                    .nullable(false),
            foreignKeyProperty(VET_SPECIALTY_SPECIALTY_FK, "Specialty", T_SPECIALTY,
                    primaryKeyProperty(VET_SPECIALTY_SPECIALTY)
                            .primaryKeyIndex(1))
                    .nullable(false))
            .caption("Vet specialties")
            .stringProvider(new StringProvider(VET_SPECIALTY_VET_FK).addText(" - ")
                    .addValue(VET_SPECIALTY_SPECIALTY_FK));
  }

  private void petType() {
    define(T_PET_TYPE,
            primaryKeyProperty(PET_TYPE_ID),
            columnProperty(PET_TYPE_NAME, Types.VARCHAR, "Name")
                    .maximumLength(80)
                    .nullable(false))
            .keyGenerator(automatic(T_PET_TYPE))
            .caption("Pet types")
            .searchPropertyIds(PET_TYPE_NAME)
            .stringProvider(new StringProvider(PET_TYPE_NAME))
            .orderBy(orderBy().ascending(PET_TYPE_NAME))
            .smallDataset(true);
  }

  private void owner() {
    define(T_OWNER,
            primaryKeyProperty(OWNER_ID),
            columnProperty(OWNER_FIRST_NAME, Types.VARCHAR, "First name")
                    .maximumLength(30)
                    .nullable(false),
            columnProperty(OWNER_LAST_NAME, Types.VARCHAR, "Last name")
                    .maximumLength(30)
                    .nullable(false),
            columnProperty(OWNER_ADDRESS, Types.VARCHAR, "Address")
                    .maximumLength(255),
            columnProperty(OWNER_CITY, Types.VARCHAR, "City")
                    .maximumLength(80),
            columnProperty(OWNER_TELEPHONE, Types.VARCHAR, "Telephone")
                    .maximumLength(20))
            .keyGenerator(automatic(T_OWNER))
            .caption("Owners")
            .searchPropertyIds(OWNER_FIRST_NAME, OWNER_LAST_NAME)
            .stringProvider(new StringProvider(OWNER_LAST_NAME).addText(", ")
                    .addValue(OWNER_FIRST_NAME))
            .orderBy(orderBy().ascending(OWNER_LAST_NAME, OWNER_FIRST_NAME));
  }

  private void pet() {
    define(T_PET,
            primaryKeyProperty(PET_ID),
            columnProperty(PET_NAME, Types.VARCHAR, "Name")
                    .maximumLength(30)
                    .nullable(false),
            columnProperty(PET_BIRTH_DATE, Types.DATE, "Birth date"),
            foreignKeyProperty(PET_PET_TYPE_FK, "Pet type", T_PET_TYPE,
                    columnProperty(PET_PET_TYPE_ID))
                    .nullable(false),
            foreignKeyProperty(PET_OWNER_FK, "Owner", T_OWNER,
                    columnProperty(PET_OWNER_ID))
                    .nullable(false))
            .keyGenerator(automatic(T_PET))
            .caption("Pets")
            .searchPropertyIds(PET_NAME)
            .stringProvider(new StringProvider(PET_NAME))
            .orderBy(orderBy().ascending(PET_NAME));
  }

  private void visit() {
    define(T_VISIT,
            primaryKeyProperty(VISIT_ID),
            foreignKeyProperty(VISIT_PET_FK, "Pet", T_PET,
                    columnProperty(VISIT_PET_ID))
                    .nullable(false),
            columnProperty(VISIT_DATE, Types.DATE, "Date")
                    .nullable(false),
            columnProperty(VISIT_DESCRIPTION, Types.VARCHAR, "Description")
                    .maximumLength(255))
            .keyGenerator(automatic(T_VISIT))
            .orderBy(orderBy().ascending(VISIT_PET_ID).descending(VISIT_DATE))
            .caption("Visits");
  }
}

2. Domain unit test

package org.jminor.framework.demos.petclinic.domain;

import org.jminor.common.db.exception.DatabaseException;
import org.jminor.framework.demos.petclinic.domain.impl.ClinicImpl;
import org.jminor.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

public final class ClinicTest extends EntityTestUnit {

  public ClinicTest() {
    super(ClinicImpl.class.getName());
  }

  @Test
  void vet() throws DatabaseException {
    test(Clinic.T_VET);
  }

  @Test
  void specialty() throws DatabaseException {
    test(Clinic.T_SPECIALTY);
  }

  @Test
  void vetSpecialty() throws DatabaseException {
    test(Clinic.T_VET_SPECIALTY);
  }

  @Test
  void petType() throws DatabaseException {
    test(Clinic.T_PET_TYPE);
  }

  @Test
  void owner() throws DatabaseException {
    test(Clinic.T_OWNER);
  }

  @Test
  void pet() throws DatabaseException {
    test(Clinic.T_PET);
  }

  @Test
  void visit() throws DatabaseException {
    test(Clinic.T_VISIT);
  }
}

3. Model

package org.jminor.framework.demos.petclinic.model;

import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.swing.framework.model.SwingEntityEditModel;

import static org.jminor.framework.demos.petclinic.domain.Clinic.*;

public final class VetSpecialtyEditModel extends SwingEntityEditModel {

  public VetSpecialtyEditModel(final EntityConnectionProvider connectionProvider) {
    super(T_VET_SPECIALTY, connectionProvider);
    setPersistValue(VET_SPECIALTY_VET_FK, false);
    setPersistValue(VET_SPECIALTY_SPECIALTY_FK, false);
  }

  @Override
  public boolean isEntityNew() {
    return getEntity().getOriginalKey().isNull();
  }
}
package org.jminor.framework.demos.petclinic.model;

import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.swing.framework.model.SwingEntityApplicationModel;
import org.jminor.swing.framework.model.SwingEntityModel;

import static org.jminor.framework.demos.petclinic.domain.Clinic.*;

public final class PetclinicAppModel extends SwingEntityApplicationModel {

  public PetclinicAppModel(final EntityConnectionProvider connectionProvider) {
    super(connectionProvider);
    setupEntityModels(connectionProvider);
  }

  private void setupEntityModels(final EntityConnectionProvider connectionProvider) {
    SwingEntityModel ownersModel = new SwingEntityModel(T_OWNER, connectionProvider);
    SwingEntityModel petsModel = new SwingEntityModel(T_PET, connectionProvider);
    SwingEntityModel visitModel = new SwingEntityModel(T_VISIT, connectionProvider);

    ownersModel.addDetailModel(petsModel);
    petsModel.addDetailModel(visitModel);

    addEntityModels(ownersModel);
  }
}

4. UI

package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.*;

public final class OwnerEditPanel extends EntityEditPanel {

  public OwnerEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(OWNER_FIRST_NAME);

    createTextField(OWNER_FIRST_NAME).setColumns(12);
    createTextField(OWNER_LAST_NAME).setColumns(12);
    createTextField(OWNER_ADDRESS).setColumns(12);
    createTextField(OWNER_CITY).setColumns(12);
    createTextField(OWNER_TELEPHONE).setColumns(12);

    setLayout(new GridLayout(3, 2, 5, 5));

    addPropertyPanel(OWNER_FIRST_NAME);
    addPropertyPanel(OWNER_LAST_NAME);
    addPropertyPanel(OWNER_ADDRESS);
    addPropertyPanel(OWNER_CITY);
    addPropertyPanel(OWNER_TELEPHONE);
  }
}
package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.common.ui.Components;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityComboBox;
import org.jminor.swing.framework.ui.EntityEditPanel;
import org.jminor.swing.framework.ui.EntityPanelBuilder;

import javax.swing.Action;
import javax.swing.JPanel;
import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.*;

public final class PetEditPanel extends EntityEditPanel {

  public PetEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(PET_NAME);

    createForeignKeyComboBox(PET_OWNER_FK);
    createTextField(PET_NAME).setColumns(12);
    createTextField(PET_BIRTH_DATE);
    final EntityComboBox petTypeBox = createForeignKeyComboBox(PET_PET_TYPE_FK);

    final Action newPetTypeAction = EntityEditPanel.createEditPanelAction(petTypeBox,
            new EntityPanelBuilder(T_PET_TYPE)
                    .setEditPanelClass(PetTypeEditPanel.class));
    final JPanel petTypePanel = Components.createEastButtonPanel(petTypeBox, newPetTypeAction, false);

    setLayout(new GridLayout(2, 2, 5, 5));

    addPropertyPanel(PET_OWNER_FK);
    addPropertyPanel(PET_NAME);
    addPropertyPanel(PET_BIRTH_DATE);
    add(createPropertyPanel(PET_PET_TYPE_FK, petTypePanel));
  }
}
package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.PET_TYPE_NAME;

public final class PetTypeEditPanel extends EntityEditPanel {

  public PetTypeEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(PET_TYPE_NAME);

    createTextField(PET_TYPE_NAME).setColumns(12);

    setLayout(new GridLayout(1, 1, 5, 5));

    addPropertyPanel(PET_TYPE_NAME);
  }
}
package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.SPECIALTY_NAME;

public final class SpecialtyEditPanel extends EntityEditPanel {

  public SpecialtyEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(SPECIALTY_NAME);

    createTextField(SPECIALTY_NAME).setColumns(12);

    setLayout(new GridLayout(1, 1, 5, 5));

    addPropertyPanel(SPECIALTY_NAME);
  }
}
package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.VET_FIRST_NAME;
import static org.jminor.framework.demos.petclinic.domain.Clinic.VET_LAST_NAME;

public final class VetEditPanel extends EntityEditPanel {

  public VetEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(VET_FIRST_NAME);

    createTextField(VET_FIRST_NAME).setColumns(12);
    createTextField(VET_LAST_NAME).setColumns(12);

    setLayout(new GridLayout(1, 2, 5, 5));

    addPropertyPanel(VET_FIRST_NAME);
    addPropertyPanel(VET_LAST_NAME);
  }
}
package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.VET_SPECIALTY_SPECIALTY_FK;
import static org.jminor.framework.demos.petclinic.domain.Clinic.VET_SPECIALTY_VET_FK;

public final class VetSpecialtyEditPanel extends EntityEditPanel {

  public VetSpecialtyEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(VET_SPECIALTY_VET_FK);

    createForeignKeyComboBox(VET_SPECIALTY_VET_FK);
    createForeignKeyComboBox(VET_SPECIALTY_SPECIALTY_FK);

    setLayout(new GridLayout(1, 2, 5, 5));

    addPropertyPanel(VET_SPECIALTY_VET_FK);
    addPropertyPanel(VET_SPECIALTY_SPECIALTY_FK);
  }
}
package org.jminor.framework.demos.petclinic.ui;

import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.ui.EntityEditPanel;

import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.BorderLayout;
import java.awt.GridLayout;

import static org.jminor.framework.demos.petclinic.domain.Clinic.*;

public final class VisitEditPanel extends EntityEditPanel {

  public VisitEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusProperty(VISIT_PET_FK);

    createForeignKeyComboBox(VISIT_PET_FK);
    createTextField(VISIT_DATE);
    createTextArea(VISIT_DESCRIPTION, 4, 20);

    JPanel northPanel = new JPanel(new GridLayout(1, 2, 5, 5));
    northPanel.add(createPropertyPanel(VISIT_PET_FK));
    northPanel.add(createPropertyPanel(VISIT_DATE));

    setLayout(new BorderLayout(5, 5));
    add(northPanel, BorderLayout.NORTH);
    add(createPropertyPanel(VISIT_DESCRIPTION,
            new JScrollPane(getComponent(VISIT_DESCRIPTION))), BorderLayout.CENTER);
  }
}

4.1. Main application panel

package org.jminor.framework.demos.petclinic.ui;

import org.jminor.common.model.CancelException;
import org.jminor.common.model.table.ColumnConditionModel;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.demos.petclinic.model.PetclinicAppModel;
import org.jminor.framework.demos.petclinic.model.VetSpecialtyEditModel;
import org.jminor.framework.model.EntityEditModel;
import org.jminor.swing.common.ui.Windows;
import org.jminor.swing.framework.model.SwingEntityModel;
import org.jminor.swing.framework.model.SwingEntityModelBuilder;
import org.jminor.swing.framework.ui.EntityApplicationPanel;
import org.jminor.swing.framework.ui.EntityEditPanel;
import org.jminor.swing.framework.ui.EntityPanel;
import org.jminor.swing.framework.ui.EntityPanelBuilder;
import org.jminor.swing.framework.ui.EntityTablePanel;

import java.util.List;
import java.util.Locale;

import static java.util.Collections.singletonList;
import static org.jminor.framework.demos.petclinic.domain.Clinic.*;

public final class PetclinicAppPanel extends EntityApplicationPanel<PetclinicAppModel> {

  @Override
  protected PetclinicAppModel initializeApplicationModel(final EntityConnectionProvider connectionProvider) {
    return new PetclinicAppModel(connectionProvider);
  }

  @Override
  protected List<EntityPanel> initializeEntityPanels(final PetclinicAppModel applicationModel) {
    SwingEntityModel ownersModel = applicationModel.getEntityModel(T_OWNER);
    SwingEntityModel petsModel = ownersModel.getDetailModel(T_PET);
    SwingEntityModel visitsModel = petsModel.getDetailModel(T_VISIT);

    EntityPanel ownersPanel = new EntityPanel(ownersModel,
            new OwnerEditPanel(ownersModel.getEditModel()));
    EntityPanel petsPanel = new EntityPanel(petsModel,
            new PetEditPanel(petsModel.getEditModel()));
    EntityPanel visitsPanel = new EntityPanel(visitsModel,
            new VisitEditPanel(visitsModel.getEditModel()));

    ownersPanel.addDetailPanel(petsPanel);
    petsPanel.addDetailPanel(visitsPanel);

    ownersModel.refresh();

    return singletonList(ownersPanel);
  }

  @Override
  protected void setupEntityPanelBuilders() {
    EntityPanelBuilder petTypePanelBuilder =
            new EntityPanelBuilder(T_PET_TYPE)
                    .setEditPanelClass(PetTypeEditPanel.class)
                    .setCaption("Pet types");
    EntityPanelBuilder specialtiesPanelBuilder =
            new EntityPanelBuilder(T_SPECIALTY)
                    .setEditPanelClass(SpecialtyEditPanel.class)
                    .setCaption("Specialties");

    EntityPanelBuilder vetsPanelBuilder =
            new EntityPanelBuilder(T_VET)
                    .setEditPanelClass(VetEditPanel.class)
                    .setCaption("Vets");
    SwingEntityModelBuilder vetSpecialtyModelBuilder =
            new SwingEntityModelBuilder(T_VET_SPECIALTY)
                    .setEditModelClass(VetSpecialtyEditModel.class);
    EntityPanelBuilder vetSpecialtiesPanelBuilder =
            new EntityPanelBuilder(vetSpecialtyModelBuilder)
                    .setEditPanelClass(VetSpecialtyEditPanel.class)
                    .setCaption("Specialties");
    vetsPanelBuilder.addDetailPanelBuilder(vetSpecialtiesPanelBuilder);

    addSupportPanelBuilders(petTypePanelBuilder, specialtiesPanelBuilder, vetsPanelBuilder);
  }

  public static void main(final String[] args) throws CancelException {
    Locale.setDefault(new Locale("en", "EN"));
    EntityEditModel.POST_EDIT_EVENTS.set(true);
    EntityEditPanel.USE_SAVE_CONTROL.set(false);
    EntityPanel.COMPACT_ENTITY_PANEL_LAYOUT.set(true);
    EntityTablePanel.REFERENTIAL_INTEGRITY_ERROR_HANDLING.set(EntityTablePanel.ReferentialIntegrityErrorHandling.DEPENDENCIES);
    ColumnConditionModel.AUTOMATIC_WILDCARD.set(ColumnConditionModel.AutomaticWildcard.POSTFIX);
    ColumnConditionModel.CASE_SENSITIVE.set(false);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("org.jminor.framework.demos.petclinic.domain.impl.ClinicImpl");
    new PetclinicAppPanel().startApplication("Petclinic", null, false,
            Windows.getScreenSizeRatio(0.6), Users.parseUser("scott:tiger"));
  }
}