All Downloads are FREE. Search and download functionalities are using the official Maven repository.

is.codion.swing.framework.model.SwingEntityTableModel Maven / Gradle / Ivy

There is a newer version: 0.18.25
Show newest version
/*
 * This file is part of Codion.
 *
 * Codion is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Codion is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Codion.  If not, see .
 *
 * Copyright (c) 2008 - 2024, Björn Darri Sigurðsson.
 */
package is.codion.swing.framework.model;

import is.codion.common.Conjunction;
import is.codion.common.Operator;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.model.UserPreferences;
import is.codion.common.model.table.ColumnConditionModel;
import is.codion.common.model.table.ColumnSummaryModel.SummaryValueProvider;
import is.codion.common.model.table.TableConditionModel;
import is.codion.common.model.table.TableSummaryModel;
import is.codion.common.state.State;
import is.codion.common.state.StateObserver;
import is.codion.common.value.Value;
import is.codion.common.value.ValueSet;
import is.codion.framework.db.EntityConnection.Select;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.OrderBy;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.framework.domain.entity.attribute.AttributeDefinition;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ColumnDefinition;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.framework.model.EntityConditionModelFactory;
import is.codion.framework.model.EntityEditEvents;
import is.codion.framework.model.EntityModel;
import is.codion.framework.model.EntityTableConditionModel;
import is.codion.framework.model.EntityTableModel;
import is.codion.framework.model.EntityTableModel.ColumnPreferences.ConditionPreferences;
import is.codion.swing.common.model.component.table.FilteredTableColumn;
import is.codion.swing.common.model.component.table.FilteredTableColumnModel;
import is.codion.swing.common.model.component.table.FilteredTableModel;
import is.codion.swing.common.model.component.table.FilteredTableSearchModel;
import is.codion.swing.common.model.component.table.FilteredTableSelectionModel;
import is.codion.swing.common.model.component.table.FilteredTableSortModel;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import java.awt.Color;
import java.text.Format;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;

import static is.codion.framework.model.EntityTableConditionModel.entityTableConditionModel;
import static is.codion.framework.model.EntityTableModel.ColumnPreferences.ConditionPreferences.conditionPreferences;
import static is.codion.framework.model.EntityTableModel.ColumnPreferences.columnPreferences;
import static is.codion.swing.common.model.component.table.FilteredTableModel.summaryValueProvider;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

/**
 * A TableModel implementation for displaying and working with entities.
 */
public class SwingEntityTableModel implements EntityTableModel, FilteredTableModel> {

  private static final Logger LOG = LoggerFactory.getLogger(SwingEntityTableModel.class);

  private static final String COLUMN_PREFERENCES = "-columns";
  private static final String CONDITIONS_PREFERENCES = "-conditions";

  private final FilteredTableModel> tableModel;
  private final SwingEntityEditModel editModel;
  private final EntityTableConditionModel> conditionModel;
  private final ValueSet> attributes = ValueSet.valueSet();
  private final State conditionRequired = State.state();
  private final State handleEditEvents = State.state();
  private final State editable = State.state();
  private final Value limit = Value.value();
  private final State queryHiddenColumns = State.state(EntityTableModel.QUERY_HIDDEN_COLUMNS.get());
  private final State orderQueryBySortOrder = State.state(ORDER_QUERY_BY_SORT_ORDER.get());
  private final State removeDeleted = State.state(true);
  private final Value onInsert = Value.value(EntityTableModel.ON_INSERT.get(), EntityTableModel.ON_INSERT.get());

  /**
   * Caches java.awt.Color instances parsed from hex strings via {@link #toColor(Object)}
   */
  private final Map colorCache = new ConcurrentHashMap<>();
  private final State conditionChanged = State.state();
  private final Consumer> updateListener = new UpdateListener();

  private Select refreshCondition;

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param entityType the entityType
   * @param connectionProvider the connection provider
   */
  public SwingEntityTableModel(EntityType entityType, EntityConnectionProvider connectionProvider) {
    this(new SwingEntityEditModel(entityType, connectionProvider));
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param entityType the entityType
   * @param connectionProvider the connection provider
   * @param columnFactory the table column factory
   */
  public SwingEntityTableModel(EntityType entityType, EntityConnectionProvider connectionProvider,
                               ColumnFactory> columnFactory) {
    this(new SwingEntityEditModel(entityType, connectionProvider), columnFactory);
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param entityType the entityType
   * @param connectionProvider the connection provider
   * @param conditionModelFactory the table condition model factory
   */
  public SwingEntityTableModel(EntityType entityType, EntityConnectionProvider connectionProvider,
                               EntityConditionModelFactory conditionModelFactory) {
    this(new SwingEntityEditModel(entityType, connectionProvider), conditionModelFactory);
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param entityType the entityType
   * @param connectionProvider the connection provider
   * @param columnFactory the table column factory
   * @param conditionModelFactory the table condition model factory
   */
  public SwingEntityTableModel(EntityType entityType, EntityConnectionProvider connectionProvider,
                               ColumnFactory> columnFactory,
                               EntityConditionModelFactory conditionModelFactory) {
    this(new SwingEntityEditModel(entityType, connectionProvider), columnFactory, conditionModelFactory);
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param editModel the edit model
   */
  public SwingEntityTableModel(SwingEntityEditModel editModel) {
    this(editModel, new SwingEntityColumnFactory(requireNonNull(editModel).entityDefinition()));
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param editModel the edit model
   * @param columnFactory the table column factory
   */
  public SwingEntityTableModel(SwingEntityEditModel editModel, ColumnFactory> columnFactory) {
    this(editModel, columnFactory, new SwingEntityConditionModelFactory(requireNonNull(editModel).connectionProvider()));
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param editModel the edit model
   * @param conditionModelFactory the table condition model factory
   */
  public SwingEntityTableModel(SwingEntityEditModel editModel, EntityConditionModelFactory conditionModelFactory) {
    this(editModel, new SwingEntityColumnFactory(requireNonNull(editModel).entityDefinition()), conditionModelFactory);
  }

  /**
   * Instantiates a new SwingEntityTableModel.
   * @param editModel the edit model
   * @param columnFactory the table column factory
   * @param conditionModelFactory the table condition model factory
   */
  public SwingEntityTableModel(SwingEntityEditModel editModel,
                               ColumnFactory> columnFactory,
                               EntityConditionModelFactory conditionModelFactory) {
    this.editModel = requireNonNull(editModel);
    this.tableModel = createTableModel(editModel.entityDefinition(), requireNonNull(columnFactory));
    this.conditionModel = entityTableConditionModel(editModel.entityType(), editModel.connectionProvider(), requireNonNull(conditionModelFactory));
    this.refreshCondition = createSelect(conditionModel);
    this.attributes.addValidator(new AttributeValidator());
    bindEvents();
    applyPreferences();
    handleEditEvents.set(true);
  }

  @Override
  public final Entities entities() {
    return editModel.connectionProvider().entities();
  }

  @Override
  public final EntityDefinition entityDefinition() {
    return editModel.entityDefinition();
  }

  @Override
  public final String toString() {
    return getClass().getSimpleName() + ": " + editModel.entityType();
  }

  @Override
  public final ValueSet> attributes() {
    return attributes;
  }

  @Override
  public final Value limit() {
    return limit;
  }

  @Override
  public final State queryHiddenColumns() {
    return queryHiddenColumns;
  }

  @Override
  public final State orderQueryBySortOrder() {
    return orderQueryBySortOrder;
  }

  @Override
  public final State conditionRequired() {
    return conditionRequired;
  }

  @Override
  public final Value onInsert() {
    return onInsert;
  }

  @Override
  public final State removeDeleted() {
    return removeDeleted;
  }

  @Override
  public final State handleEditEvents() {
    return handleEditEvents;
  }

  @Override
  public final EntityType entityType() {
    return editModel.entityType();
  }

  @Override
  public final EntityTableConditionModel> conditionModel() {
    return conditionModel;
  }

  @Override
  public final  C editModel() {
    return (C) editModel;
  }

  @Override
  public final EntityConnectionProvider connectionProvider() {
    return editModel.connectionProvider();
  }

  @Override
  public final State editable() {
    return editable;
  }

  /**
   * Returns true if the cell at rowIndex and modelColumnIndex is editable.
   * @param rowIndex the row to edit
   * @param modelColumnIndex the model index of the column to edit
   * @return true if the cell is editable
   * @see #setValueAt(Object, int, int)
   */
  @Override
  public boolean isCellEditable(int rowIndex, int modelColumnIndex) {
    if (!editable.get() || editModel.readOnly().get() || !editModel.updateEnabled().get()) {
      return false;
    }
    Attribute attribute = columnModel().columnIdentifier(modelColumnIndex);
    if (attribute instanceof ForeignKey) {
      return entityDefinition().foreignKeys().updatable((ForeignKey) attribute);
    }

    AttributeDefinition attributeDefinition = entityDefinition().attributes().definition(attribute);

    return attributeDefinition instanceof ColumnDefinition && ((ColumnDefinition) attributeDefinition).updatable();
  }

  /**
   * Sets the value in the given cell and updates the underlying Entity.
   * @param value the new value
   * @param rowIndex the row whose value is to be changed
   * @param modelColumnIndex the model index of the column to be changed
   */
  @Override
  public final void setValueAt(Object value, int rowIndex, int modelColumnIndex) {
    if (!editable.get() || editModel.readOnly().get() || !editModel.updateEnabled().get()) {
      throw new IllegalStateException("This table model is readOnly or has disabled update");
    }
    Entity entity = itemAt(rowIndex).copy();
    Attribute columnIdentifier = columnModel().columnIdentifier(modelColumnIndex);
    entity.put((Attribute) columnIdentifier, value);
    try {
      editModel.update(singletonList(entity));
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public Color backgroundColor(int row, Attribute attribute) {
    requireNonNull(attribute);
    Object color = entityDefinition().backgroundColorProvider().color(itemAt(row), attribute);

    return color == null ? null : toColor(color);
  }

  @Override
  public Color foregroundColor(int row, Attribute attribute) {
    requireNonNull(attribute);
    Object color = entityDefinition().foregroundColorProvider().color(itemAt(row), attribute);

    return color == null ? null : toColor(color);
  }

  @Override
  public final Optional find(Entity.Key primaryKey) {
    requireNonNull(primaryKey);
    return visibleItems().stream()
            .filter(entity -> entity.primaryKey().equals(primaryKey))
            .findFirst();
  }

  @Override
  public final int indexOf(Entity.Key primaryKey) {
    return find(primaryKey)
            .map(this::indexOf)
            .orElse(-1);
  }

  @Override
  public final void replace(Collection entities) {
    replaceEntitiesByKey(Entity.mapToPrimaryKey(entities));
  }

  @Override
  public final void refresh(Collection keys) {
    try {
      replace(connectionProvider().connection().select(keys));
    }
    catch (DatabaseException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public final void replace(ForeignKey foreignKey, Collection foreignKeyValues) {
    requireNonNull(foreignKey, "foreignKey");
    requireNonNull(foreignKeyValues, "foreignKeyValues");
    entityDefinition().foreignKeys().definition(foreignKey);
    boolean changed = false;
    for (Entity entity : items()) {
      for (Entity foreignKeyValue : foreignKeyValues) {
        Entity currentForeignKeyValue = entity.referencedEntity(foreignKey);
        if (currentForeignKeyValue != null && currentForeignKeyValue.equals(foreignKeyValue)) {
          entity.put(foreignKey, foreignKeyValue.immutable());
          changed = true;
        }
      }
    }
    if (changed) {
      fireTableRowsUpdated(0, getRowCount() - 1);
    }
  }

  @Override
  public final void select(Collection keys) {
    selectionModel().setSelectedItems(new SelectByKeyPredicate(requireNonNull(keys, "keys")));
  }

  @Override
  public final Collection find(Collection keys) {
    requireNonNull(keys, "keys");
    return items().stream()
            .filter(entity -> keys.contains(entity.primaryKey()))
            .collect(toList());
  }

  @Override
  public final void deleteSelected() throws DatabaseException {
    editModel.delete(selectionModel().getSelectedItems());
  }

  @Override
  public final void setVisibleColumns(Attribute... attributes) {
    columnModel().setVisibleColumns(attributes);
  }

  @Override
  public final void setVisibleColumns(List> attributes) {
    columnModel().setVisibleColumns(attributes);
  }

  @Override
  public final void savePreferences() {
    if (EntityModel.USE_CLIENT_PREFERENCES.get()) {
      try {
        UserPreferences.setUserPreference(userPreferencesKey() + COLUMN_PREFERENCES,
                ColumnPreferences.toString(createColumnPreferences()));
      }
      catch (Exception e) {
        LOG.error("Error while saving column preferences", e);
      }
      try {
        UserPreferences.setUserPreference(userPreferencesKey() + CONDITIONS_PREFERENCES,
                ConditionPreferences.toString(createConditionPreferences()));
      }
      catch (Exception e) {
        LOG.error("Error while saving condition preferences", e);
      }
    }
  }

  @Override
  public final StateObserver conditionChanged() {
    return conditionChanged.observer();
  }

  @Override
  public final void addSelectionListener(Runnable listener) {
    selectionModel().addSelectionListener(listener);
  }

  @Override
  public final void filterItems() {
    tableModel.filterItems();
  }

  @Override
  public final Value> includeCondition() {
    return tableModel.includeCondition();
  }

  @Override
  public final Collection items() {
    return tableModel.items();
  }

  @Override
  public final List visibleItems() {
    return tableModel.visibleItems();
  }

  @Override
  public final Collection filteredItems() {
    return tableModel.filteredItems();
  }

  @Override
  public final int visibleCount() {
    return tableModel.visibleCount();
  }

  @Override
  public final int filteredCount() {
    return tableModel.filteredCount();
  }

  @Override
  public final boolean containsItem(Entity item) {
    return tableModel.containsItem(item);
  }

  @Override
  public final boolean visible(Entity item) {
    return tableModel.visible(item);
  }

  @Override
  public final boolean filtered(Entity item) {
    return tableModel.filtered(item);
  }

  @Override
  public final Refresher refresher() {
    return tableModel.refresher();
  }

  @Override
  public final void refresh() {
    tableModel.refresh();
  }

  @Override
  public final void refreshThen(Consumer> afterRefresh) {
    tableModel.refreshThen(afterRefresh);
  }

  @Override
  public final void clear() {
    tableModel.clear();
  }

  @Override
  public final int getRowCount() {
    return tableModel.getRowCount();
  }

  @Override
  public final int indexOf(Entity item) {
    return tableModel.indexOf(item);
  }

  @Override
  public final Entity itemAt(int rowIndex) {
    return tableModel.itemAt(rowIndex);
  }

  @Override
  public final Object getValueAt(int rowIndex, int columnIndex) {
    return tableModel.getValueAt(rowIndex, columnIndex);
  }

  @Override
  public final String getStringAt(int rowIndex, Attribute columnIdentifier) {
    return tableModel.getStringAt(rowIndex, columnIdentifier);
  }

  @Override
  public final void addItems(Collection items) {
    tableModel.addItems(items);
  }

  @Override
  public final void addItemsSorted(Collection items) {
    tableModel.addItemsSorted(items);
  }

  @Override
  public final void addItemsAt(int index, Collection items) {
    tableModel.addItemsAt(index, items);
  }

  @Override
  public final void addItemsAtSorted(int index, Collection items) {
    tableModel.addItemsAtSorted(index, items);
  }

  @Override
  public final void addItem(Entity item) {
    tableModel.addItem(item);
  }

  @Override
  public final void addItemAt(int index, Entity item) {
    tableModel.addItemAt(index, item);
  }

  @Override
  public final void addItemSorted(Entity item) {
    tableModel.addItemSorted(item);
  }

  @Override
  public final void setItemAt(int index, Entity item) {
    tableModel.setItemAt(index, item);
  }

  @Override
  public final void removeItems(Collection items) {
    tableModel.removeItems(items);
  }

  @Override
  public final void removeItem(Entity item) {
    tableModel.removeItem(item);
  }

  @Override
  public final Entity removeItemAt(int index) {
    return tableModel.removeItemAt(index);
  }

  @Override
  public final List removeItems(int fromIndex, int toIndex) {
    return tableModel.removeItems(fromIndex, toIndex);
  }

  @Override
  public final void fireTableDataChanged() {
    tableModel.fireTableDataChanged();
  }

  @Override
  public void fireTableRowsUpdated(int fromIndex, int toIndex) {
    tableModel.fireTableRowsUpdated(fromIndex, toIndex);
  }

  @Override
  public final FilteredTableColumnModel> columnModel() {
    return tableModel.columnModel();
  }

  @Override
  public final  Collection values(Attribute columnIdentifier) {
    return tableModel.values(columnIdentifier);
  }

  @Override
  public final Class getColumnClass(Attribute columnIdentifier) {
    return tableModel.getColumnClass(columnIdentifier);
  }

  @Override
  public final  Collection selectedValues(Attribute columnIdentifier) {
    return tableModel.selectedValues(columnIdentifier);
  }

  @Override
  public final String rowsAsDelimitedString(char delimiter) {
    return tableModel.rowsAsDelimitedString(delimiter);
  }

  @Override
  public final State mergeOnRefresh() {
    return tableModel.mergeOnRefresh();
  }

  @Override
  public final void sortItems() {
    tableModel.sortItems();
  }

  @Override
  public final FilteredTableSelectionModel selectionModel() {
    return tableModel.selectionModel();
  }

  @Override
  public final FilteredTableSortModel> sortModel() {
    return tableModel.sortModel();
  }

  @Override
  public final FilteredTableSearchModel searchModel() {
    return tableModel.searchModel();
  }

  @Override
  public final TableConditionModel> filterModel() {
    return tableModel.filterModel();
  }

  @Override
  public final TableSummaryModel> summaryModel() {
    return tableModel.summaryModel();
  }

  @Override
  public final int getColumnCount() {
    return tableModel.getColumnCount();
  }

  @Override
  public final String getColumnName(int columnIndex) {
    return tableModel.getColumnName(columnIndex);
  }

  @Override
  public final Class getColumnClass(int columnIndex) {
    return tableModel.getColumnClass(columnIndex);
  }

  @Override
  public final void addDataChangedListener(Runnable listener) {
    tableModel.addDataChangedListener(listener);
  }

  @Override
  public final void removeDataChangedListener(Runnable listener) {
    tableModel.removeDataChangedListener(listener);
  }

  @Override
  public final void addClearListener(Runnable listener) {
    tableModel.addClearListener(listener);
  }

  @Override
  public final void removeClearListener(Runnable listener) {
    tableModel.removeClearListener(listener);
  }

  @Override
  public final void addRowsRemovedListener(Consumer listener) {
    tableModel.addRowsRemovedListener(listener);
  }

  @Override
  public final void removeRowsRemovedListener(Consumer listener) {
    tableModel.removeRowsRemovedListener(listener);
  }

  @Override
  public final void addTableModelListener(TableModelListener listener) {
    tableModel.addTableModelListener(listener);
  }

  @Override
  public final void removeTableModelListener(TableModelListener listener) {
    tableModel.removeTableModelListener(listener);
  }

  /**
   * @param entities the entities to display
   * @param connectionProvider the connection provider
   * @return a static {@link SwingEntityTableModel} instance containing the given entities
   * @throws IllegalArgumentException in case {@code entities} is empty
   */
  public static SwingEntityTableModel tableModel(Collection entities, EntityConnectionProvider connectionProvider) {
    if (requireNonNull(entities).isEmpty()) {
      throw new IllegalArgumentException("One or more entities is required for a static table model");
    }

    SwingEntityTableModel tableModel = new SwingEntityTableModel(entities.iterator().next().entityType(), connectionProvider) {
      @Override
      protected Collection refreshItems() {
        return entities;
      }
    };
    tableModel.refresh();

    return tableModel;
  }

  /**
   * Queries the data used to populate this EntityTableModel when it is refreshed.
   * This method should take into account the where and having conditions
   * ({@link EntityTableConditionModel#where(Conjunction)}, {@link EntityTableConditionModel#having(Conjunction)}),
   * order by clause ({@link #orderBy()}), the limit ({@link #limit()}) and select attributes
   * ({@link #attributes()}) when querying.
   * @return entities selected from the database according to the query condition.
   * @see #conditionRequired()
   * @see #conditionEnabled(EntityTableConditionModel)
   * @see EntityTableConditionModel#where(Conjunction)
   * @see EntityTableConditionModel#having(Conjunction)
   */
  protected Collection refreshItems() {
    try {
      return queryItems();
    }
    catch (DatabaseException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * It can be necessary to prevent the user from selecting too much data, when working with a large dataset.
   * This can be done by enabling the {@link #conditionRequired()}, which prevents a refresh as long as this
   * method returns {@code false}. This default implementation simply returns {@link EntityTableConditionModel#enabled()}.
   * Override for a more fine grained control, such as requiring a specific column condition to be enabled.
   * @param conditionModel the table condition model
   * @return true if enough conditions are enabled for a safe refresh
   * @see #conditionRequired()
   */
  protected boolean conditionEnabled(EntityTableConditionModel> conditionModel) {
    return conditionModel.enabled();
  }

  /**
   * Returns a {@link java.awt.Color} instance from the given Object.
   * {@link java.awt.Color} instances are returned as-is, but instances of
   * {@link java.lang.String} are assumed to be in HEX format (f.ex: #ffff00" or #00ff00)
   * and are parsed with {@link Color#decode(String)}. Colors parsed from Strings are cached.
   * Override to support other representations.
   * @param color the object representing the color.
   * @return a {@link java.awt.Color} instance based on the given Object
   * @throws IllegalArgumentException in case the representation is not supported
   * @throws NullPointerException in case color is null
   */
  protected Color toColor(Object color) {
    requireNonNull(color);
    if (color instanceof Color) {
      return (Color) color;
    }
    if (color instanceof String) {
      return colorCache.computeIfAbsent((String) color, Color::decode);
    }

    throw new IllegalArgumentException("Unsupported Color representation: " + color);
  }

  /**
   * The order by clause to use when selecting the data for this model.
   * If ordering by sort order is enabled a {@link OrderBy} clause is constructed
   * according to the sort order of column based attributes, otherwise the order by
   * clause defined for the underlying entity is returned.
   * @return the order by clause
   * @see #orderQueryBySortOrder()
   * @see EntityDefinition#orderBy()
   */
  protected OrderBy orderBy() {
    if (orderQueryBySortOrder.get() && sortModel().sorted()) {
      OrderBy orderBy = orderByFromSortModel();
      if (!orderBy.orderByColumns().isEmpty()) {
        return orderBy;
      }
    }

    return entityDefinition().orderBy().orElse(null);
  }

  /**
   * Returns the key used to identify user preferences for this table model, that is column positions, widths and such.
   * The default implementation is:
   * 
   * {@code
   * return getClass().getSimpleName() + "-" + entityType();
   * }
   * 
* Override in case this key is not unique. * @return the key used to identify user preferences for this table model */ protected String userPreferencesKey() { return getClass().getSimpleName() + "-" + entityType(); } /** * Clears any user preferences saved for this table model */ final void clearPreferences() { String userPreferencesKey = userPreferencesKey(); UserPreferences.removeUserPreference(userPreferencesKey + COLUMN_PREFERENCES); UserPreferences.removeUserPreference(userPreferencesKey + CONDITIONS_PREFERENCES); } private void bindEvents() { columnModel().addColumnHiddenListener(this::onColumnHidden); handleEditEvents.addDataListener(new HandleEditEventsListener()); conditionModel.addChangeListener(() -> onConditionChanged(createSelect(conditionModel))); editModel.addAfterInsertListener(this::onInsert); editModel.addAfterUpdateListener(this::onUpdate); editModel.addAfterDeleteListener(this::onDelete); editModel.addEntityListener(this::onEntitySet); selectionModel().addSelectedItemListener(editModel::set); addTableModelListener(this::onTableModelEvent); } private List queryItems() throws DatabaseException { Select select = createSelect(conditionModel); if (conditionRequired.get() && !conditionEnabled(conditionModel)) { updateRefreshSelect(select); return emptyList(); } List items = editModel.connectionProvider().connection().select(select); updateRefreshSelect(select); return items; } private void updateRefreshSelect(Select select) { refreshCondition = select; conditionChanged.set(false); } private void onInsert(Collection insertedEntities) { Collection entitiesToAdd = insertedEntities.stream() .filter(entity -> entity.entityType().equals(entityType())) .collect(toList()); if (!onInsert.equalTo(OnInsert.DO_NOTHING) && !entitiesToAdd.isEmpty()) { if (!selectionModel().isSelectionEmpty()) { selectionModel().clearSelection(); } switch (onInsert.get()) { case ADD_TOP: tableModel.addItemsAt(0, entitiesToAdd); break; case ADD_TOP_SORTED: tableModel.addItemsAtSorted(0, entitiesToAdd); break; case ADD_BOTTOM: tableModel.addItemsAt(visibleCount(), entitiesToAdd); break; case ADD_BOTTOM_SORTED: tableModel.addItemsAtSorted(visibleCount(), entitiesToAdd); break; default: break; } } } private void onUpdate(Map updatedEntities) { replaceEntitiesByKey(new HashMap<>(updatedEntities)); } private void onDelete(Collection deletedEntities) { if (removeDeleted.get()) { removeItems(deletedEntities); } } private void onEntitySet(Entity entity) { if (entity == null && !selectionModel().isSelectionEmpty()) { selectionModel().clearSelection(); } } private void onTableModelEvent(TableModelEvent tableModelEvent) { //if the selected row is updated via the table model, refresh the one in the edit model if (tableModelEvent.getType() == TableModelEvent.UPDATE && tableModelEvent.getFirstRow() == selectionModel().getSelectedIndex()) { editModel.set(selectionModel().getSelectedItem()); } } private void onConditionChanged(Select condition) { conditionChanged.set(!Objects.equals(refreshCondition, condition)); } private void onColumnHidden(Attribute attribute) { //disable the condition and filter model for the column to be hidden, to prevent confusion ColumnConditionModel columnConditionModel = conditionModel.conditionModels().get(attribute); if (columnConditionModel != null && !columnConditionModel.locked().get()) { columnConditionModel.enabled().set(false); } ColumnConditionModel filterModel = filterModel().conditionModels().get(attribute); if (filterModel != null && !filterModel.locked().get()) { filterModel.enabled().set(false); } } /** * Replace the entities identified by the Entity.Key map keys with their respective value. * Note that this does not trigger {@link #filterItems()}, that must be done explicitly. * @param entitiesByKey the entities to replace mapped to the corresponding primary key found in this table model */ private void replaceEntitiesByKey(Map entitiesByKey) { for (Entity entity : items()) { Iterator> iterator = entitiesByKey.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); if (entity.primaryKey().equals(entry.getKey())) { iterator.remove(); entity.set(entry.getValue()); int index = indexOf(entity); if (index >= 0) { fireTableRowsUpdated(index, index); } } } if (entitiesByKey.isEmpty()) { break; } } } private OrderBy orderByFromSortModel() { OrderBy.Builder builder = OrderBy.builder(); sortModel().columnSortOrder().stream() .filter(columnSortOrder -> isColumn(columnSortOrder.columnIdentifier())) .forEach(columnSortOrder -> { switch (columnSortOrder.sortOrder()) { case ASCENDING: builder.ascending((Column) columnSortOrder.columnIdentifier()); break; case DESCENDING: builder.descending((Column) columnSortOrder.columnIdentifier()); break; default: } }); return builder.build(); } private boolean isColumn(Attribute attribute) { return entityDefinition().attributes().definition(attribute) instanceof ColumnDefinition; } private Map, ColumnPreferences> createColumnPreferences() { Map, ColumnPreferences> columnPreferencesMap = new HashMap<>(); for (FilteredTableColumn> column : columnModel().columns()) { Attribute attribute = column.getIdentifier(); int index = columnModel().visible(attribute).get() ? columnModel().getColumnIndex(attribute) : -1; columnPreferencesMap.put(attribute, columnPreferences(attribute, index, column.getWidth())); } return columnPreferencesMap; } private Map, ConditionPreferences> createConditionPreferences() { Map, ConditionPreferences> conditionPreferencesMap = new HashMap<>(); for (FilteredTableColumn> column : columnModel().columns()) { Attribute attribute = column.getIdentifier(); ColumnConditionModel columnConditionModel = conditionModel.conditionModels().get(attribute); if (columnConditionModel != null) { conditionPreferencesMap.put(attribute, conditionPreferences(attribute, columnConditionModel.autoEnable().get(), columnConditionModel.caseSensitive().get(), columnConditionModel.automaticWildcard().get())); } } return conditionPreferencesMap; } private void applyPreferences() { if (EntityModel.USE_CLIENT_PREFERENCES.get()) { String columnPreferencesString = UserPreferences.getUserPreference(userPreferencesKey() + COLUMN_PREFERENCES, ""); if (columnPreferencesString.isEmpty()) {//todo remove: see if a legacy one without "-columns" postfix exists columnPreferencesString = UserPreferences.getUserPreference(userPreferencesKey(), ""); } if (!columnPreferencesString.isEmpty()) { applyColumnPreferences(columnPreferencesString); } String conditionPreferencesString = UserPreferences.getUserPreference(userPreferencesKey() + CONDITIONS_PREFERENCES, ""); if (!conditionPreferencesString.isEmpty()) { applyConditionPreferences(conditionPreferencesString); } } } private void applyColumnPreferences(String preferencesString) { List> columnAttributes = columnModel().columns().stream() .map(FilteredTableColumn::getIdentifier) .collect(toList()); try { ColumnPreferences.apply(this, columnAttributes, preferencesString, (attribute, columnWidth) -> columnModel().column(attribute).setPreferredWidth(columnWidth)); } catch (Exception e) { LOG.error("Error while applying column preferences: " + preferencesString, e); } } private void applyConditionPreferences(String preferencesString) { List> columnAttributes = columnModel().columns().stream() .map(FilteredTableColumn::getIdentifier) .collect(toList()); try { ConditionPreferences.apply(this, columnAttributes, preferencesString); } catch (Exception e) { LOG.error("Error while applying condition preferences: " + preferencesString, e); } } private Select createSelect(EntityTableConditionModel> conditionModel) { return Select.where(conditionModel.where(Conjunction.AND)) .having(conditionModel.having(Conjunction.AND)) .attributes(selectAttributes()) .limit(limit().optional().orElse(-1)) .orderBy(orderBy()) .build(); } private Collection> selectAttributes() { FilteredTableColumnModel> columnModel = columnModel(); if (queryHiddenColumns.get() || columnModel.hidden().isEmpty()) { return attributes.get(); } return entityDefinition().attributes().selected().stream() .filter(this::columnNotHidden) .collect(toList()); } private boolean columnNotHidden(Attribute attribute) { return !columnModel().containsColumn(attribute) || columnModel().visible(attribute).get(); } private class AttributeValidator implements Value.Validator>> { @Override public void validate(Set> attributes) { for (Attribute attribute : attributes) { if (!attribute.entityType().equals(entityType())) { throw new IllegalArgumentException(attribute + " is not part of entity: " + entityType()); } } } } private final class UpdateListener implements Consumer> { @Override public void accept(Map updated) { updated.values().stream() .collect(groupingBy(Entity::entityType, HashMap::new, toList())) .forEach((entityType, entities) -> entityDefinition().foreignKeys().get(entityType).forEach(foreignKey -> replace(foreignKey, entities))); } } private final class HandleEditEventsListener implements Consumer { @Override public void accept(Boolean listen) { if (listen) { addEditListeners(); } else { removeEditListeners(); } } private void addEditListeners() { entityDefinition().foreignKeys().get().forEach(foreignKey -> EntityEditEvents.addUpdateListener(foreignKey.referencedType(), updateListener)); } private void removeEditListeners() { entityDefinition().foreignKeys().get().forEach(foreignKey -> EntityEditEvents.removeUpdateListener(foreignKey.referencedType(), updateListener)); } } private static final class SelectByKeyPredicate implements Predicate { private final List keyList; private SelectByKeyPredicate(Collection keys) { this.keyList = new ArrayList<>(keys); } @Override public boolean test(Entity entity) { if (keyList.isEmpty()) { return false; } int index = keyList.indexOf(entity.primaryKey()); if (index >= 0) { keyList.remove(index); return true; } return false; } } private FilteredTableModel> createTableModel(EntityDefinition entityDefinition, ColumnFactory> columnFactory) { return FilteredTableModel.builder(columnFactory, new EntityColumnValueProvider()) .filterModelFactory(new EntityFilterModelFactory(entityDefinition)) .summaryValueProviderFactory(new EntitySummaryValueProviderFactory(entityDefinition, this)) .itemSupplier(new EntityItemSupplier(this)) .itemValidator(new EntityItemValidator(entityDefinition.entityType())) .build(); } private static final class EntityColumnValueProvider implements ColumnValueProvider> { @Override public Object value(Entity entity, Attribute attribute) { return entity.get(attribute); } @Override public String string(Entity entity, Attribute attribute) { return entity.string(attribute); } @Override public Comparable comparable(Entity entity, Attribute attribute) { Object value = entity.get(attribute); if (value instanceof Entity) { return (Comparable) value.toString(); } return (Comparable) value; } } private static final class EntityFilterModelFactory implements ColumnConditionModel.Factory> { private final EntityDefinition entityDefinition; private EntityFilterModelFactory(EntityDefinition entityDefinition) { this.entityDefinition = requireNonNull(entityDefinition); } @Override public Optional, ?>> createConditionModel(Attribute attribute) { AttributeDefinition attributeDefinition = entityDefinition.attributes().definition(attribute); if (attributeDefinition.hidden() || !Comparable.class.isAssignableFrom(attribute.type().valueClass())) { return Optional.empty(); } if (requireNonNull(attribute).type().isEntity() || !attributeDefinition.items().isEmpty()) { return Optional.of(ColumnConditionModel.builder(attribute, String.class) .operators(operators(String.class)) .build()); } return Optional.of(ColumnConditionModel.builder(attribute, attribute.type().valueClass()) .operators(operators(attribute.type().valueClass())) .format(attributeDefinition.format()) .dateTimePattern(attributeDefinition.dateTimePattern()) .build()); } private static List operators(Class columnClass) { if (columnClass.equals(Boolean.class)) { return singletonList(Operator.EQUAL); } return Arrays.asList(Operator.values()); } } private static final class EntitySummaryValueProviderFactory implements SummaryValueProvider.Factory> { private final EntityDefinition entityDefinition; private final FilteredTableModel> tableModel; private EntitySummaryValueProviderFactory(EntityDefinition entityDefinition, FilteredTableModel> tableModel) { this.entityDefinition = requireNonNull(entityDefinition); this.tableModel = requireNonNull(tableModel); } @Override public Optional> createSummaryValueProvider(Attribute attribute, Format format) { AttributeDefinition attributeDefinition = entityDefinition.attributes().definition(attribute); if (attribute.type().isNumerical() && attributeDefinition.items().isEmpty()) { return Optional.of(summaryValueProvider(attribute, tableModel, format)); } return Optional.empty(); } } private static final class EntityItemSupplier implements Supplier> { private final SwingEntityTableModel tableModel; private EntityItemSupplier(SwingEntityTableModel tableModel) { this.tableModel = requireNonNull(tableModel); } @Override public Collection get() { return tableModel.refreshItems(); } } private static final class EntityItemValidator implements Predicate { private final EntityType entityType; private EntityItemValidator(EntityType entityType) { this.entityType = requireNonNull(entityType); } @Override public boolean test(Entity entity) { return entity.entityType().equals(entityType); } } }