io.github.jonestimd.swing.table.model.BufferedHeaderDetailTableModel Maven / Gradle / Ivy
// Copyright (c) 2019 Timothy D. Jones
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package io.github.jonestimd.swing.table.model;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import io.github.jonestimd.swing.validation.BeanPropertyValidator;
import io.github.jonestimd.util.Streams;
/**
* This class overrides {@link HeaderDetailTableModel} to add change tracking and validation. Changes to cells are queued
* by {@link #setValueAt}. The following methods are used to queue pending changes for rows:
*
* - {@link #queueAdd(Object)}
* - {@link #queueAdd(int, Object)}
* - {@link #queueAppendSubRow(int)}
* - {@link #queueDelete(int)}
*
* The {@link #commit()} method should be called after all pending changes have been saved.
* The {@link #revert()} method is used to revert all pending changes.
*/
public class BufferedHeaderDetailTableModel extends HeaderDetailTableModel
implements ChangeBufferTableModel, ValidatedTableModel
{
private final Table errors = HashBasedTable.create();
private ChangeTracker> changeTracker = new ChangeTracker>(true) {
@Override
protected void revertItemChange(Object originalValue, ChangeRow row, int index) {
int rowIndex = rowIndexOf(row.header) + detailAdapter.detailIndex(row.header, row.detail) + 1;
setCellValue(originalValue, rowIndex, index);
}
@Override
protected void itemUpdated(ChangeRow row) {
fireTableRowsUpdated(row);
}
@Override
protected void itemDeleted(ChangeRow row) {
if (row.detail == null) {
BufferedHeaderDetailTableModel.super.removeBean(row.header);
}
else {
removeSubRow(row);
}
}
};
protected BufferedHeaderDetailTableModel(DetailAdapter detailAdapter, Function super H, ?> idFunction) {
super(detailAdapter, idFunction);
}
public BufferedHeaderDetailTableModel(DetailAdapter detailAdapter,
List extends ColumnAdapter> columnAdapters,
List extends List extends ColumnAdapter, ?>>> detailColumnAdapters) {
this(detailAdapter, Function.identity(), columnAdapters, detailColumnAdapters);
}
public BufferedHeaderDetailTableModel(DetailAdapter detailAdapter, Function super H, ?> idFunction,
List extends ColumnAdapter> columnAdapters,
List extends List extends ColumnAdapter, ?>>> detailColumnAdapters) {
super(detailAdapter, idFunction, columnAdapters, detailColumnAdapters);
}
/**
* Overridden to reset change tracking and validation.
*/
@Override
public void setBeans(Collection beans) {
changeTracker.reset();
errors.clear();
super.setBeans(beans);
for (int i = 0; i < getBeanCount(); i++) {
updateGroupValidation(i);
}
}
@Override
public boolean queueDelete(H bean) {
return queueDelete(rowIndexOf(bean));
}
/**
* Mark a row as a pending delete or remove the row if it is a pending addition. If the specified row is the
* header of a group then the operation is applied to the entire group.
* @return true if the delete was queued or false if the row was an unsaved addition and was deleted immediately.
*/
public boolean queueDelete(int rowIndex) {
int subRowIndex = getSubRowIndex(rowIndex);
H bean = getBeanAtRow(rowIndex);
if (subRowIndex > 0 && isPendingAdd(rowIndex)) {
changeTracker.resetItem(new ChangeRow<>(bean, detailAdapter.getDetail(bean, subRowIndex-1)));
detailAdapter.removeDetail(bean, subRowIndex-1);
fireTableRowsDeleted(rowIndex, rowIndex);
return false;
}
if (isPendingAdd(rowIndex)) {
removeBean(bean);
return false;
}
queueDelete(bean, subRowIndex);
return true;
}
private void queueDelete(H bean, int subRowIndex) {
if (subRowIndex == 0) {
changeTracker.cancelDeletes(new HeaderPredicate(bean));
}
ChangeRow row = new ChangeRow<>(bean, subRowIndex == 0 ? null : detailAdapter.getDetail(bean, subRowIndex-1));
changeTracker.pendingDelete(row);
fireTableRowsUpdated(row);
}
/**
* Append an unsaved group.
*/
public void queueAdd(H bean) {
queueAdd(getBeanCount(), bean);
}
/**
* Insert an unsaved group.
*/
public void queueAdd(int groupNumber, H bean) {
changeTracker.pendingAdd(new ChangeRow<>(bean, null));
addBean(groupNumber, bean);
}
/**
* Append a detail row to a group.
* @param currentRow the index of a row in the group.
* @return the index of the new row.
*/
public int queueAppendSubRow(int currentRow) {
int index = getGroupNumber(currentRow);
H bean = getBean(index);
int subRowCount = detailAdapter.appendDetail(bean);
fireSubRowInserted(bean, subRowCount);
return getLeadRowForGroup(index) + subRowCount;
}
/**
* Append a saved group.
*/
public void addBean(H bean) {
addBean(getBeanCount(), bean);
}
/**
* Insert a saved group.
*/
@Override
public void addBean(int index, H bean) {
super.addBean(index, bean);
int firstRow = getLeadRowForGroup(index);
int subRowCount = detailAdapter.getDetailCount(bean);
updateGroupValidation(index);
fireTableRowsUpdated(firstRow, firstRow + subRowCount);
}
private void shiftErrors(int firstRow, int delta) {
Table updatedErrors = HashBasedTable.create();
Iterator>> iterator = errors.rowMap().entrySet().iterator();
while (iterator.hasNext()) {
Entry> entry = iterator.next();
if (entry.getKey() >= firstRow) {
updatedErrors.row(entry.getKey() + delta).putAll(entry.getValue());
iterator.remove();
}
}
errors.putAll(updatedErrors);
}
/**
* Overridden to reset pending changes and validation for the group.
*/
@Override
public void setBean(int index, H bean) {
H oldBean = getBean(index);
resetChanges(oldBean);
super.setBean(index, bean);
updateGroupValidation(index);
}
/**
* Overridden to update validation.
*/
@Override
public void fireTableRowsDeleted(int firstRow, int lastRow) {
for (int i = firstRow; i <= lastRow; i++) {
errors.row(i).clear();
}
shiftErrors(firstRow, firstRow - lastRow - 1);
super.fireTableRowsDeleted(firstRow, lastRow);
}
private H resetChanges(H bean) {
changeTracker.resetItems(new HeaderPredicate(bean));
return bean;
}
@Override
public void removeBean(H bean) {
super.removeBean(bean);
resetChanges(bean);
}
// TODO visible for testing
protected void removeSubRow(H bean, Object subRow) {
removeSubRow(new ChangeRow<>(bean, subRow));
}
private void removeSubRow(ChangeRow row) {
int rowIndex = rowIndexOf(row.header) + detailAdapter.detailIndex(row.header, row.detail) + 1;
detailAdapter.removeDetail(row.header, row.detail);
fireTableRowsDeleted(rowIndex, rowIndex);
}
/**
* Overridden to update validation.
*/
@Override
public void fireTableRowsInserted(int firstRow, int lastRow) {
shiftErrors(firstRow, lastRow - firstRow + 1);
super.fireTableRowsInserted(firstRow, lastRow);
}
/**
* Mark the sub-row as an unsaved addition unless the header bean is already an unsaved addition.
*/
protected void fireSubRowInserted(H bean, int subRowIndex) {
int index = indexOf(bean);
if (! changeTracker.isPendingAdd(new ChangeRow<>(bean, null))) {
changeTracker.pendingAdd(new ChangeRow<>(bean, detailAdapter.getDetail(bean, subRowIndex-1)));
}
int rowIndex = getLeadRowForGroup(index) + subRowIndex;
fireTableRowsInserted(rowIndex, rowIndex);
}
/**
* Update the errors for all rows in a group without firing change events.
*/
protected void updateGroupValidation(int groupNumber) {
int headerRow = getLeadRowForGroup(groupNumber);
H bean = getBean(groupNumber);
for (int subRow = 0; subRow <= detailAdapter.getDetailCount(bean); subRow++) {
updateRowValidation(headerRow + subRow);
}
}
/**
* Update the errors for a row without firing change events.
*/
protected void updateRowValidation(int rowIndex) {
for (int columnIndex = 0; columnIndex < getColumnCount(); columnIndex++) {
validateCell(rowIndex, columnIndex);
}
}
/**
* @return true if the cell validation changed
*/
protected boolean validateCell(int rowIndex, int columnIndex) {
String validation = validateAt(rowIndex, columnIndex, getValueAt(rowIndex, columnIndex));
if (validation == null) {
return errors.remove(rowIndex, columnIndex) != null;
}
return errors.put(rowIndex, columnIndex, validation) == null;
}
@Override
public String validateAt(int rowIndex, int columnIndex) {
return errors.get(rowIndex, columnIndex);
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public String validateAt(int rowIndex, int columnIndex, V value) {
int subRowIndex = getSubRowIndex(rowIndex);
if (subRowIndex == 0) {
ColumnAdapter columnAdapter = getColumnAdapter(columnIndex);
if (columnAdapter instanceof BeanPropertyValidator) {
BeanPropertyValidator validator = (BeanPropertyValidator) columnAdapter;
return validator.validate(getGroupNumber(rowIndex), value, getBeans());
}
}
else {
H bean = getBeanAtRow(rowIndex);
int detailTypeIndex = detailAdapter.getDetailTypeIndex(bean, subRowIndex-1);
ColumnAdapter
© 2015 - 2024 Weber Informatics LLC | Privacy Policy