Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
is.codion.swing.framework.model.SwingEntityTableModel Maven / Gradle / Ivy
/*
* 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, Attribute>> tableModel;
private EntitySummaryValueProviderFactory(EntityDefinition entityDefinition, FilteredTableModel, Attribute>> 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);
}
}
}