1. Domain

package is.codion.framework.demos.petstore.domain;

import is.codion.framework.domain.DefaultDomain;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.Attribute;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.ForeignKey;

import java.math.BigDecimal;

import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.KeyGenerator.increment;
import static is.codion.framework.domain.entity.OrderBy.orderBy;
import static is.codion.framework.domain.entity.StringFactory.stringFactory;
import static is.codion.framework.domain.property.Properties.*;

public final class Petstore extends DefaultDomain {

  static final DomainType DOMAIN = domainType(Petstore.class);

  public Petstore() {
    super(DOMAIN);
    address();
    category();
    product();
    sellerContactInfo();
    item();
    tag();
    tagItem();
  }

  public interface Address {
    EntityType<Entity> TYPE = DOMAIN.entityType("address");

    Attribute<Integer> ID = TYPE.integerAttribute("Address id");
    Attribute<String> STREET_1 = TYPE.stringAttribute("Street 1");
    Attribute<String> STREET_2 = TYPE.stringAttribute("Street 2");
    Attribute<String> CITY = TYPE.stringAttribute("City");
    Attribute<String> STATE = TYPE.stringAttribute("State");
    Attribute<Integer> ZIP = TYPE.integerAttribute("Zip");
    Attribute<Double> LATITUDE = TYPE.doubleAttribute("Latitude");
    Attribute<Double> LONGITUDE = TYPE.doubleAttribute("Longitude");
  }

  void address() {
    define(Address.TYPE, "petstore.address",
            primaryKeyProperty(Address.ID)
                    .columnName("addressid"),
            columnProperty(Address.STREET_1, Address.STREET_1.getName())
                    .columnName("street1").maximumLength(55).nullable(false),
            columnProperty(Address.STREET_2, Address.STREET_2.getName())
                    .columnName("street2").maximumLength(55),
            columnProperty(Address.CITY, Address.CITY.getName())
                    .columnName("city").maximumLength(55).nullable(false),
            columnProperty(Address.STATE, Address.STATE.getName())
                    .columnName("state").maximumLength(25).nullable(false),
            columnProperty(Address.ZIP, Address.ZIP.getName())
                    .columnName("zip").nullable(false),
            columnProperty(Address.LATITUDE, Address.LATITUDE.getName())
                    .columnName("latitude").nullable(false).maximumFractionDigits(2),
            columnProperty(Address.LONGITUDE, Address.LONGITUDE.getName())
                    .columnName("longitude").nullable(false).maximumFractionDigits(2))
            .keyGenerator(increment("petstore.address", "addressid"))
            .orderBy(orderBy().ascending(Address.CITY, Address.STREET_1, Address.STREET_2))
            .stringFactory(stringFactory(Address.STREET_1).text(" ")
                    .value(Address.STREET_2).text(", ").value(Address.CITY).text(" ")
                    .value(Address.ZIP).text(", ").value(Address.STATE))
            .caption("Addresses");
  }

  public interface Category {
    EntityType<Entity> TYPE = DOMAIN.entityType("category");

    Attribute<Integer> ID = TYPE.integerAttribute("Category id");
    Attribute<String> NAME = TYPE.stringAttribute("Name");
    Attribute<String> DESCRIPTION = TYPE.stringAttribute("Description");
    Attribute<String> IMAGE_URL = TYPE.stringAttribute("Image URL");
  }

  void category() {
    define(Category.TYPE, "petstore.category",
            primaryKeyProperty(Category.ID)
                    .columnName("categoryid"),
            columnProperty(Category.NAME, Category.NAME.getName())
                    .columnName("name").maximumLength(25).nullable(false),
            columnProperty(Category.DESCRIPTION, Category.DESCRIPTION.getName())
                    .columnName("description").maximumLength(255).nullable(false),
            columnProperty(Category.IMAGE_URL, Category.IMAGE_URL.getName())
                    .columnName("imageurl").hidden())
            .keyGenerator(increment("petstore.category", "categoryid"))
            .orderBy(orderBy().ascending(Category.NAME))
            .stringFactory(stringFactory(Category.NAME))
            .caption("Categories");
  }

  public interface Product {
    EntityType<Entity> TYPE = DOMAIN.entityType("product");

    Attribute<Integer> ID = TYPE.integerAttribute("Product id");
    Attribute<Integer> CATEGORY_ID = TYPE.integerAttribute("Category id");
    Attribute<String> NAME = TYPE.stringAttribute("Name");
    Attribute<String> DESCRIPTION = TYPE.stringAttribute("Description");
    Attribute<String> IMAGE_URL = TYPE.stringAttribute("Image URL");

    ForeignKey CATEGORY_FK = TYPE.foreignKey("Category", Product.CATEGORY_ID, Category.ID);
  }

  void product() {
    define(Product.TYPE, "petstore.product",
            primaryKeyProperty(Product.ID)
                    .columnName("productid"),
            columnProperty(Product.CATEGORY_ID)
                    .columnName("categoryid").nullable(false),
            foreignKeyProperty(Product.CATEGORY_FK, Product.CATEGORY_FK.getName()),
            columnProperty(Product.NAME, Product.NAME.getName())
                    .columnName("name").maximumLength(25).nullable(false),
            columnProperty(Product.DESCRIPTION, Product.DESCRIPTION.getName())
                    .columnName("description").maximumLength(255).nullable(false),
            columnProperty(Product.IMAGE_URL, Product.IMAGE_URL.getName())
                    .columnName("imageurl").maximumLength(55).hidden())
            .keyGenerator(increment("petstore.product", "productid"))
            .orderBy(orderBy().ascending(Product.NAME))
            .stringFactory(stringFactory(Product.CATEGORY_FK)
                    .text(" - ").value(Product.NAME))
            .caption("Products");
  }

  public interface SellerContactInfo {
    EntityType<Entity> TYPE = DOMAIN.entityType("sellercontactinfo");

    Attribute<Integer> ID = TYPE.integerAttribute("Contactinfo id");
    Attribute<String> FIRST_NAME = TYPE.stringAttribute("First name");
    Attribute<String> LAST_NAME = TYPE.stringAttribute("Last name");
    Attribute<String> EMAIL = TYPE.stringAttribute("Email");
  }

  void sellerContactInfo() {
    define(SellerContactInfo.TYPE, "petstore.sellercontactinfo",
            primaryKeyProperty(SellerContactInfo.ID)
                    .columnName("contactinfoid"),
            columnProperty(SellerContactInfo.FIRST_NAME, SellerContactInfo.FIRST_NAME.getName())
                    .searchProperty().columnName("firstname").maximumLength(24).nullable(false),
            columnProperty(SellerContactInfo.LAST_NAME, SellerContactInfo.LAST_NAME.getName())
                    .searchProperty().columnName("lastname").maximumLength(24).nullable(false),
            columnProperty(SellerContactInfo.EMAIL, SellerContactInfo.EMAIL.getName())
                    .columnName("email").maximumLength(24).nullable(false))
            .keyGenerator(increment("petstore.sellercontactinfo", "contactinfoid"))
            .orderBy(orderBy()
                    .ascending(SellerContactInfo.LAST_NAME, SellerContactInfo.FIRST_NAME))
            .stringFactory(stringFactory(SellerContactInfo.LAST_NAME)
                    .text(", ").value(SellerContactInfo.FIRST_NAME))
            .caption("Seller info");
  }

  public interface Item {
    EntityType<Entity> TYPE = DOMAIN.entityType("item");

    Attribute<Integer> ID = TYPE.integerAttribute("Item id");
    Attribute<Integer> PRODUCT_ID = TYPE.integerAttribute("Product id");
    Attribute<String> NAME = TYPE.stringAttribute("Name");
    Attribute<String> DESCRIPTION = TYPE.stringAttribute("Description");
    Attribute<String> IMAGE_URL = TYPE.stringAttribute("Image URL");
    Attribute<String> IMAGE_THUMB_URL = TYPE.stringAttribute("Image thumbnail URL");
    Attribute<BigDecimal> PRICE = TYPE.bigDecimalAttribute("Price");
    Attribute<Integer> CONTACT_INFO_ID = TYPE.integerAttribute("Contactinfo id");
    Attribute<Integer> ADDRESS_ID = TYPE.integerAttribute("Address id");
    Attribute<Boolean> DISABLED = TYPE.booleanAttribute("Disabled");

    ForeignKey PRODUCT_FK = TYPE.foreignKey("Product", Item.PRODUCT_ID, Product.ID);
    ForeignKey CONTACT_INFO_FK = TYPE.foreignKey("Contact info", Item.CONTACT_INFO_ID, SellerContactInfo.ID);
    ForeignKey ADDRESS_FK = TYPE.foreignKey("Address", Item.ADDRESS_ID, Address.ID);
  }

  void item() {
    define(Item.TYPE, "petstore.item",
            primaryKeyProperty(Item.ID)
                    .columnName("itemid"),
            columnProperty(Item.PRODUCT_ID)
                    .columnName("productid").nullable(false),
            foreignKeyProperty(Item.PRODUCT_FK, Item.PRODUCT_FK.getName())
                    .fetchDepth(2),
            columnProperty(Item.NAME, Item.NAME.getName())
                    .columnName("name").maximumLength(30).nullable(false),
            columnProperty(Item.DESCRIPTION, Item.DESCRIPTION.getName())
                    .columnName("description").maximumLength(500).nullable(false),
            columnProperty(Item.IMAGE_URL, Item.IMAGE_URL.getName())
                    .columnName("imageurl").maximumLength(55).hidden(),
            columnProperty(Item.IMAGE_THUMB_URL, Item.IMAGE_THUMB_URL.getName())
                    .columnName("imagethumburl").maximumLength(55).hidden(),
            columnProperty(Item.PRICE, Item.PRICE.getName())
                    .columnName("price").nullable(false).maximumFractionDigits(2),
            columnProperty(Item.CONTACT_INFO_ID).columnName("contactinfo_contactinfoid")
                    .nullable(false),
            foreignKeyProperty(Item.CONTACT_INFO_FK, Item.CONTACT_INFO_FK.getName()),
            columnProperty(Item.ADDRESS_ID).columnName("address_addressid")
                    .nullable(false),
            foreignKeyProperty(Item.ADDRESS_FK, "Address"),
            booleanProperty(Item.DISABLED, Item.DISABLED.getName(), Integer.class, 1, 0)
                    .columnName("disabled").defaultValue(false))
            .keyGenerator(increment("petstore.item", "itemid"))
            .orderBy(orderBy().ascending(Item.NAME))
            .stringFactory(stringFactory(Item.PRODUCT_FK)
                    .text(" - ").value(Item.NAME))
            .caption("Items");
  }

  public interface Tag {
    EntityType<Entity> TYPE = DOMAIN.entityType("tag");

    Attribute<Integer> ID = TYPE.integerAttribute("Tag id");
    Attribute<String> TAG = TYPE.stringAttribute("Tag");
    Attribute<Integer> REFCOUNT = TYPE.integerAttribute("Reference count");
  }

  void tag() {
    define(Tag.TYPE, "petstore.tag",
            primaryKeyProperty(Tag.ID)
                    .columnName("tagid"),
            columnProperty(Tag.TAG, Tag.TAG.getName())
                    .columnName("tag").maximumLength(30).nullable(false),
            subqueryProperty(Tag.REFCOUNT, Tag.REFCOUNT.getName(),
                    "select count(*) from petstore.tag_item where tagid = tag.tagid")
                    .columnName("refcount"))
            .keyGenerator(increment("petstore.tag", "tagid"))
            .orderBy(orderBy().ascending(Tag.TAG))
            .selectTableName("petstore.tag tag")
            .stringFactory(stringFactory(Tag.TAG))
            .caption("Tags");
  }

  public interface TagItem {
    EntityType<Entity> TYPE = DOMAIN.entityType("tag_item");

    Attribute<Integer> ITEM_ID = TYPE.integerAttribute("Item id");
    Attribute<Integer> TAG_ID = TYPE.integerAttribute("Tag id");

    ForeignKey ITEM_FK = TYPE.foreignKey("Item", TagItem.ITEM_ID, Item.ID);
    ForeignKey TAG_FK = TYPE.foreignKey("Tag", TagItem.TAG_ID, Tag.ID);
  }

  void tagItem() {
    define(TagItem.TYPE, "petstore.tag_item",
            columnProperty(TagItem.ITEM_ID)
                    .columnName("itemid").primaryKeyIndex(0),
            foreignKeyProperty(TagItem.ITEM_FK, TagItem.ITEM_FK.getName())
                    .fetchDepth(3),
            columnProperty(TagItem.TAG_ID)
                    .columnName("tagid").primaryKeyIndex(1),
            foreignKeyProperty(TagItem.TAG_FK, TagItem.TAG_FK.getName()))
            .stringFactory(stringFactory(TagItem.ITEM_FK)
                    .text(" - ").value(TagItem.TAG_FK))
            .caption("Item tags");
  }
}

1.1. Domain unit test

package is.codion.framework.demos.petstore.domain;

import is.codion.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

import static is.codion.framework.demos.petstore.domain.Petstore.*;

public class PetstoreTest extends EntityTestUnit {

  public PetstoreTest() {
    super(Petstore.class.getName());
  }

  @Test
  void address() throws Exception {
    test(Address.TYPE);
  }

  @Test
  void category() throws Exception {
    test(Category.TYPE);
  }

  @Test
  void item() throws Exception {
    test(Item.TYPE);
  }

  @Test
  void product() throws Exception {
    test(Product.TYPE);
  }

  @Test
  void sellerInfo() throws Exception {
    test(SellerContactInfo.TYPE);
  }

  @Test
  void tag() throws Exception {
    test(Tag.TYPE);
  }

  @Test
  void tagItem() throws Exception {
    test(TagItem.TYPE);
  }
}

2. Model

2.1. Main application model

package is.codion.framework.demos.petstore.model;

import is.codion.framework.db.EntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityApplicationModel;
import is.codion.swing.framework.model.SwingEntityModel;

import static is.codion.framework.demos.petstore.domain.Petstore.*;

public final class PetstoreAppModel extends SwingEntityApplicationModel {

  public PetstoreAppModel(final EntityConnectionProvider connectionProvider) {
    super(connectionProvider);
    final SwingEntityModel categoryModel = new SwingEntityModel(Category.TYPE, connectionProvider);
    final SwingEntityModel productModel = new SwingEntityModel(Product.TYPE, connectionProvider);
    final SwingEntityModel itemModel = new SwingEntityModel(Item.TYPE, connectionProvider);
    final SwingEntityModel tagItemModel = new SwingEntityModel(TagItem.TYPE, connectionProvider);
    itemModel.addDetailModels(tagItemModel);
    productModel.addDetailModels(itemModel);
    categoryModel.addDetailModels(productModel);
    addEntityModel(categoryModel);
  }
}

3. UI

package is.codion.framework.demos.petstore.ui;

import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import javax.swing.JLabel;

import static is.codion.framework.demos.petstore.domain.Petstore.Address;

public class AddressEditPanel extends EntityEditPanel {

  public AddressEditPanel(final SwingEntityEditModel model) {
    super(model);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Address.CITY);

    createTextField(Address.CITY);
    createTextField(Address.STATE);
    createTextField(Address.ZIP);
    createTextField(Address.STREET_1);
    createTextField(Address.STREET_2);
    createTextField(Address.LATITUDE);
    createTextField(Address.LONGITUDE);

    setLayout(Layouts.flexibleGridLayout(4, 2));
    addInputPanel(Address.CITY);
    addInputPanel(Address.STATE);
    add(new JLabel());
    addInputPanel(Address.ZIP);
    addInputPanel(Address.STREET_1);
    addInputPanel(Address.STREET_2);
    addInputPanel(Address.LATITUDE);
    addInputPanel(Address.LONGITUDE);
  }
}
package is.codion.framework.demos.petstore.ui;

import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.framework.demos.petstore.domain.Petstore.Category;

public class CategoryEditPanel extends EntityEditPanel {

  public CategoryEditPanel(final SwingEntityEditModel model) {
    super(model);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Category.NAME);

    createTextField(Category.NAME);
    createTextField(Category.DESCRIPTION).columns(18);
    createTextField(Category.IMAGE_URL);

    setLayout(Layouts.flexibleGridLayout(2, 2));
    addInputPanel(Category.NAME);
    addInputPanel(Category.DESCRIPTION);
    addInputPanel(Category.IMAGE_URL);
  }
}
package is.codion.framework.demos.petstore.ui;

import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.framework.demos.petstore.domain.Petstore.SellerContactInfo;

public class ContactInfoEditPanel extends EntityEditPanel {

  public ContactInfoEditPanel(final SwingEntityEditModel model) {
    super(model);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(SellerContactInfo.LAST_NAME);

    createTextField(SellerContactInfo.LAST_NAME);
    createTextField(SellerContactInfo.FIRST_NAME);
    createTextField(SellerContactInfo.EMAIL);

    setLayout(Layouts.flexibleGridLayout(3, 1));
    addInputPanel(SellerContactInfo.LAST_NAME);
    addInputPanel(SellerContactInfo.FIRST_NAME);
    addInputPanel(SellerContactInfo.EMAIL);
  }
}
package is.codion.framework.demos.petstore.ui;

import is.codion.swing.common.ui.Components;
import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityComboBox;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;

import static is.codion.framework.demos.petstore.domain.Petstore.*;

public class ItemEditPanel extends EntityEditPanel {

  public ItemEditPanel(final SwingEntityEditModel model) {
    super(model);
    setDefaultTextFieldColumns(14);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Item.PRODUCT_FK);

    createForeignKeyComboBox(Item.PRODUCT_FK);
    createTextField(Item.NAME);
    createTextInputPanel(Item.DESCRIPTION)
            .buttonFocusable(false);
    createTextField(Item.PRICE);
    final EntityComboBox contactInfoBox = createForeignKeyComboBox(Item.CONTACT_INFO_FK)
            .preferredWidth(140)
            .popupWidth(200)
            .build();
    final EntityComboBox addressBox = createForeignKeyComboBox(Item.ADDRESS_FK)
            .popupWidth(200)
            .build();
    createTextField(Item.IMAGE_URL);
    createTextField(Item.IMAGE_THUMB_URL);
    createCheckBox(Item.DISABLED);

    setLayout(Layouts.flexibleGridLayout(3, 3));
    addInputPanel(Item.PRODUCT_FK);
    addInputPanel(Item.NAME);
    addInputPanel(Item.DESCRIPTION);
    addInputPanel(Item.PRICE);
    addInputPanel(Item.CONTACT_INFO_FK, Components.createEastButtonPanel(contactInfoBox,
            EntityPanel.builder(SellerContactInfo.TYPE)
                    .editPanelClass(ContactInfoEditPanel.class)
                    .createEditPanelAction(contactInfoBox)));
    addInputPanel(Item.ADDRESS_FK, Components.createEastButtonPanel(addressBox,
            EntityPanel.builder(Address.TYPE)
                    .editPanelClass(AddressEditPanel.class)
                    .createEditPanelAction(addressBox)));
    addInputPanel(Item.IMAGE_URL);
    addInputPanel(Item.IMAGE_THUMB_URL);
    addInputPanel(Item.DISABLED);
  }
}
package is.codion.framework.demos.petstore.ui;

import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.framework.demos.petstore.domain.Petstore.Product;

public class ProductEditPanel extends EntityEditPanel {

  public ProductEditPanel(final SwingEntityEditModel model) {
    super(model);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Product.CATEGORY_FK);

    createForeignKeyComboBox(Product.CATEGORY_FK);
    createTextField(Product.NAME);
    createTextField(Product.DESCRIPTION).columns(16);

    setLayout(Layouts.flexibleGridLayout(3, 1));
    addInputPanel(Product.CATEGORY_FK);
    addInputPanel(Product.NAME);
    addInputPanel(Product.DESCRIPTION);
  }
}
package is.codion.framework.demos.petstore.ui;

import is.codion.framework.demos.petstore.domain.Petstore.Tag;
import is.codion.framework.demos.petstore.domain.Petstore.TagItem;
import is.codion.swing.common.ui.Components;
import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityComboBox;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;

public class TagItemEditPanel extends EntityEditPanel {

  public TagItemEditPanel(final SwingEntityEditModel model) {
    super(model);
  }

  @Override
  protected void initializeUI() {
    setLayout(Layouts.flexibleGridLayout(2, 1));
    final EntityComboBox itemBox = createForeignKeyComboBox(TagItem.ITEM_FK)
            .popupWidth(240)
            .preferredWidth(180)
            .build();
    setInitialFocusComponent(itemBox);
    addInputPanel(TagItem.ITEM_FK);
    final EntityComboBox itemTagBox = createForeignKeyComboBox(TagItem.TAG_FK)
            .build();
    addInputPanel(TagItem.TAG_FK, Components.createEastButtonPanel(itemTagBox,
            EntityPanel.builder(Tag.TYPE).editPanelClass(TagEditPanel.class)
                    .createEditPanelAction(itemTagBox)));
  }
}
package is.codion.framework.demos.petstore.ui;

import is.codion.framework.demos.petstore.domain.Petstore.Tag;
import is.codion.swing.common.ui.layout.Layouts;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

public class TagEditPanel extends EntityEditPanel {

  public TagEditPanel(final SwingEntityEditModel model) {
    super(model);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Tag.TAG);

    createTextField(Tag.TAG).columns(16);

    setLayout(Layouts.flexibleGridLayout(1, 1));
    addInputPanel(Tag.TAG);
  }
}

4. Main application panel

package is.codion.framework.demos.petstore.ui;

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.petstore.model.PetstoreAppModel;
import is.codion.swing.common.ui.Windows;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityPanel;

import javax.swing.SwingUtilities;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import static is.codion.framework.demos.petstore.domain.Petstore.*;

public final class PetstoreAppPanel extends EntityApplicationPanel<PetstoreAppModel> {

  public PetstoreAppPanel() {
    super("The Pet Store");
  }

  @Override
  protected List<EntityPanel> initializeEntityPanels(final PetstoreAppModel applicationModel) {
    /* CATEGORY
     *   PRODUCT
     *     ITEM
     *       ITEMTAG
     */
    final SwingEntityModel categoryModel = applicationModel.getEntityModel(Category.TYPE);
    final SwingEntityModel productModel = categoryModel.getDetailModel(Product.TYPE);
    final SwingEntityModel itemModel = productModel.getDetailModel(Item.TYPE);
    final SwingEntityModel tagItemModel = itemModel.getDetailModel(TagItem.TYPE);

    final EntityPanel categoryPanel = new EntityPanel(categoryModel,
            new CategoryEditPanel(categoryModel.getEditModel()));
    final EntityPanel productPanel = new EntityPanel(productModel,
            new ProductEditPanel(productModel.getEditModel()));
    final EntityPanel itemPanel = new EntityPanel(itemModel,
            new ItemEditPanel(itemModel.getEditModel()));
    final EntityPanel tagItemPanel = new EntityPanel(tagItemModel,
            new TagItemEditPanel(tagItemModel.getEditModel()));

    categoryPanel.addDetailPanel(productPanel);
    categoryPanel.setDetailSplitPanelResizeWeight(0.3);
    productPanel.addDetailPanel(itemPanel);
    itemPanel.addDetailPanels(tagItemPanel);
    itemPanel.setDetailPanelState(EntityPanel.PanelState.HIDDEN);

    categoryModel.refresh();

    return Collections.singletonList(categoryPanel);
  }

  @Override
  protected List<EntityPanel.Builder> initializeSupportEntityPanelBuilders(final PetstoreAppModel applicationModel) {
    final SwingEntityModel.Builder tagModelBuilder =
            SwingEntityModel.builder(Tag.TYPE)
                    .detailModelBuilder(SwingEntityModel.builder(TagItem.TYPE));
    final SwingEntityModel.Builder sellerContactInfoModelBuilder =
            SwingEntityModel.builder(SellerContactInfo.TYPE)
                    .detailModelBuilder(SwingEntityModel.builder(Item.TYPE)
                            .detailModelBuilder(SwingEntityModel.builder(TagItem.TYPE)));

    return Arrays.asList(
            EntityPanel.builder(Address.TYPE)
                    .editPanelClass(AddressEditPanel.class),
            EntityPanel.builder(sellerContactInfoModelBuilder)
                    .editPanelClass(ContactInfoEditPanel.class)
                    .detailPanelBuilder(EntityPanel.builder(Item.TYPE)
                            .editPanelClass(ItemEditPanel.class)
                            .detailPanelBuilder(EntityPanel.builder(TagItem.TYPE)
                                    .editPanelClass(TagItemEditPanel.class))
                            .detailPanelState(EntityPanel.PanelState.HIDDEN)),
            EntityPanel.builder(tagModelBuilder)
                    .editPanelClass(TagEditPanel.class)
                    .detailPanelBuilder(EntityPanel.builder(TagItem.TYPE)
                            .editPanelClass(TagItemEditPanel.class))
                    .detailPanelState(EntityPanel.PanelState.HIDDEN));
  }

  @Override
  protected PetstoreAppModel initializeApplicationModel(final EntityConnectionProvider connectionProvider)
          throws CancelException {
    return new PetstoreAppModel(connectionProvider);
  }

  public static void main(final String[] args) {
    Locale.setDefault(new Locale("en"));
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.petstore.domain.Petstore");
    SwingUtilities.invokeLater(() -> new PetstoreAppPanel().starter()
            .frameSize(Windows.getScreenSizeRatio(0.8))
            .defaultLoginUser(User.parseUser("scott:tiger"))
            .start());
  }
}

5. Load test

package is.codion.framework.demos.petstore.testing;

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.petstore.domain.Petstore;
import is.codion.framework.demos.petstore.model.PetstoreAppModel;
import is.codion.swing.common.tools.loadtest.AbstractUsageScenario;
import is.codion.swing.common.tools.ui.loadtest.LoadTestPanel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.tools.loadtest.EntityLoadTestModel;

import javax.swing.SwingUtilities;

import static java.util.Collections.singletonList;

public final class PetstoreLoadTest extends EntityLoadTestModel<PetstoreAppModel> {

  private static final User UNIT_TEST_USER =
          User.parseUser(System.getProperty("codion.test.user", "scott:tiger"));

  public PetstoreLoadTest() {
    super(UNIT_TEST_USER, singletonList(new AbstractUsageScenario<PetstoreAppModel>("selectRecords") {
      @Override
      protected void perform(final PetstoreAppModel application) {
        final SwingEntityModel categoryModel = application.getEntityModels().iterator().next();
        categoryModel.getTableModel().getSelectionModel().clearSelection();
        categoryModel.refresh();
        selectRandomRow(categoryModel.getTableModel());
        selectRandomRow(categoryModel.getDetailModels().iterator().next().getTableModel());
        selectRandomRow(categoryModel.getDetailModels().iterator().next().getDetailModels().iterator().next().getTableModel());
      }
    }));
  }

  @Override
  protected PetstoreAppModel initializeApplication() throws CancelException {
    final PetstoreAppModel applicationModel = new PetstoreAppModel(
            EntityConnectionProvider.connectionProvider().setDomainClassName(Petstore.class.getName())
                    .setClientTypeId(getClass().getSimpleName()).setUser(getUser()));
    final SwingEntityModel categoryModel = applicationModel.getEntityModels().iterator().next();
    categoryModel.addLinkedDetailModel(categoryModel.getDetailModels().iterator().next());
    final SwingEntityModel productModel = categoryModel.getDetailModels().iterator().next();
    productModel.addLinkedDetailModel(productModel.getDetailModels().iterator().next());
    final SwingEntityModel itemModel = productModel.getDetailModels().iterator().next();
    itemModel.addLinkedDetailModel(itemModel.getDetailModels().iterator().next());

    return applicationModel;
  }

  public static void main(final String[] args) throws Exception {
    SwingUtilities.invokeLater(new Runner());
  }

  private static final class Runner implements Runnable {
    @Override
    public void run() {
      try {
        new LoadTestPanel<>(new PetstoreLoadTest()).showFrame();
      }
      catch (final Exception e) {
        e.printStackTrace();
      }
    }
  }
}