package com.revolsys.swing.map.layer.record.table.model; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dialog.ModalityType; import java.awt.Rectangle; import java.awt.Window; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.Closeable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.WindowConstants; import org.jdesktop.swingx.VerticalLayout; import org.jdesktop.swingx.decorator.ComponentAdapter; import org.jdesktop.swingx.decorator.HighlightPredicate; import org.jdesktop.swingx.decorator.HighlightPredicate.AndHighlightPredicate; import org.jdesktop.swingx.decorator.Highlighter; import com.revolsys.awt.WebColors; import com.revolsys.beans.ObjectPropertyException; import com.revolsys.record.schema.RecordDefinition; import com.revolsys.swing.SwingUtil; import com.revolsys.swing.component.BasePanel; import com.revolsys.swing.map.layer.record.AbstractRecordLayer; import com.revolsys.swing.map.layer.record.LayerRecord; import com.revolsys.swing.parallel.Invoke; import com.revolsys.swing.table.TablePanel; import com.revolsys.swing.table.highlighter.ColorHighlighter; import com.revolsys.swing.table.record.RecordRowTable; import com.revolsys.swing.table.record.model.RecordListTableModel; import com.revolsys.swing.toolbar.ToolBar; import com.revolsys.util.Property; public class RecordValidationDialog implements PropertyChangeListener, Closeable { public static void validateRecords(final String title, final AbstractRecordLayer layer, final Iterable<? extends LayerRecord> records, final Consumer<RecordValidationDialog> successAction, final Consumer<RecordValidationDialog> cancelAction) { try ( final RecordValidationDialog validator = new RecordValidationDialog(layer)) { validator.validateRecords(records, false); if (validator.invalidRecords.isEmpty()) { successAction.accept(validator); } else { validator.showErrorDialog(title, successAction, cancelAction); } } } public static void validateRecords(final String title, final AbstractRecordLayer layer, final LayerRecord record, final Consumer<RecordValidationDialog> successAction, final Consumer<RecordValidationDialog> cancelAction) { final List<LayerRecord> records = Collections.singletonList(record); validateRecords(title, layer, records, successAction, cancelAction); } private final List<Map<String, String>> invalidRecordErrors = new ArrayList<>(); /** Records that were initially invalid. Does not change when records are made valid. */ private final List<LayerRecord> invalidRecords = new ArrayList<>(); private final AbstractRecordLayer layer; /** Records that were initially valid. Does not change when records are made valid. */ private final List<LayerRecord> validRecords = new ArrayList<>(); private RecordValidationDialog(final AbstractRecordLayer layer) { this.layer = layer; Property.addListener(layer, this); } protected void addInvalidRecords(final List<LayerRecord> records, final boolean withErrors) { for (int i = 0; i < this.invalidRecords.size(); i++) { final Map<String, String> errors = this.invalidRecordErrors.get(i); final boolean hasErrors = !errors.isEmpty(); if (hasErrors == withErrors) { final LayerRecord record = this.invalidRecords.get(i); records.add(record); } } } private synchronized void addRecordFieldError(final LayerRecord record, final String fieldName, final String errorMessage) { int recordIndex = getInvalidRecordIndex(record); Map<String, String> fieldErrors; if (recordIndex == -1) { recordIndex = this.invalidRecords.size(); this.invalidRecords.add(record); fieldErrors = new HashMap<>(); this.invalidRecordErrors.add(fieldErrors); } else { fieldErrors = this.invalidRecordErrors.get(recordIndex); } final String oldErrors = fieldErrors.get(fieldName); if (Property.hasValue(oldErrors)) { final String mergedErrorMessage = oldErrors + "\n" + errorMessage; fieldErrors.put(fieldName, mergedErrorMessage); } else { fieldErrors.put(fieldName, errorMessage); } } @Override public void close() { Property.removeListener(this.layer, this); } private int getInvalidRecordIndex(final LayerRecord record) { for (int i = this.invalidRecords.size() - 1; i >= 0; i--) { final LayerRecord invalidRecord = this.invalidRecords.get(i); if (record.isSame(invalidRecord)) { return i; } } return -1; } /** * Get the list of records that were still invalid after editing. * * @return The list of records. */ public List<LayerRecord> getInvalidRecords() { final List<LayerRecord> records = new ArrayList<>(); addInvalidRecords(records, true); return records; } /** * Get the list of records that were still valid after editing. * * @return The list of records. */ public List<LayerRecord> getValidRecords() { final List<LayerRecord> records = new ArrayList<>(this.validRecords); addInvalidRecords(records, false); return records; } private TablePanel newInvalidRecordsTablePanel() { final RecordDefinition recordDefinition = this.layer.getRecordDefinition(); final List<String> fieldNames = this.layer.getFieldNames(); final RecordListTableModel model = new RecordListTableModel(recordDefinition, this.invalidRecords, fieldNames); model.setReadOnlyFieldNames(this.layer.getUserReadOnlyFieldNames()); final RecordRowTable table = new RecordRowTable(model); table.setVisibleRowCount(Math.min(10, model.getRowCount() + 1)); table.setSortable(true); table.resizeColumnsToContent(); final HighlightPredicate invalidFieldPredicate = (final Component renderer, final ComponentAdapter adapter) -> { try { final int rowIndex = adapter.convertRowIndexToModel(adapter.row); final int columnIndex = adapter.convertColumnIndexToModel(adapter.column); final Map<String, String> fieldErrors = this.invalidRecordErrors.get(rowIndex); if (!fieldErrors.isEmpty()) { final String fieldName = this.layer.getFieldName(columnIndex); final String errorMessage = fieldErrors.get(fieldName); if (Property.hasValue(errorMessage)) { final JComponent jcomponent = (JComponent)renderer; jcomponent.setToolTipText(errorMessage); return true; } } } catch (final Throwable e) { } return false; }; final Highlighter invalidFieldHighlighter = new ColorHighlighter(invalidFieldPredicate, WebColors.newAlpha(Color.RED, 64), Color.RED, Color.RED, Color.YELLOW); table.addHighlighter(invalidFieldHighlighter); final HighlightPredicate validRecordPredicate = (final Component renderer, final ComponentAdapter adapter) -> { try { final int rowIndex = adapter.convertRowIndexToModel(adapter.row); final Map<String, String> fieldErrors = this.invalidRecordErrors.get(rowIndex); if (fieldErrors.isEmpty()) { return true; } } catch (final Throwable e) { } return false; }; table.addHighlighter( new ColorHighlighter(new AndHighlightPredicate(validRecordPredicate, HighlightPredicate.EVEN), WebColors.newAlpha(WebColors.LimeGreen, 127), WebColors.Black, WebColors.newAlpha(WebColors.DarkGreen, 191), Color.WHITE)); table.addHighlighter( new ColorHighlighter(new AndHighlightPredicate(validRecordPredicate, HighlightPredicate.ODD), WebColors.LimeGreen, WebColors.Black, WebColors.DarkGreen, Color.WHITE)); final TablePanel tablePanel = new TablePanel(table); tablePanel .setBorder(BorderFactory.createTitledBorder(table.getRowCount() + " invalid records")); return tablePanel; } private TablePanel newValidRecordsTablePanel() { final RecordDefinition recordDefinition = this.layer.getRecordDefinition(); final List<String> fieldNames = this.layer.getFieldNames(); final RecordListTableModel model = new RecordListTableModel(recordDefinition, this.validRecords, fieldNames); final RecordRowTable table = new RecordRowTable(model); table.setVisibleRowCount(Math.min(10, model.getRowCount() + 1)); table.setSortable(true); table.setEditable(false); table.resizeColumnsToContent(); final TablePanel tablePanel = new TablePanel(table); tablePanel.setBorder(BorderFactory.createTitledBorder(table.getRowCount() + " valid records")); return tablePanel; } @Override public void propertyChange(final PropertyChangeEvent e) { final Object source = e.getSource(); if (source instanceof LayerRecord) { final LayerRecord record = (LayerRecord)source; if (this.layer.isLayerRecord(record)) { final String fieldName = e.getPropertyName(); final int fieldIndex = record.getFieldIndex(fieldName); validateField(record, fieldIndex); } } } private void showErrorDialog(final String title, final Consumer<RecordValidationDialog> successAction, final Consumer<RecordValidationDialog> cancelAction) { Invoke.andWait(() -> { final String layerPath = this.layer.getPath(); final Window window = SwingUtil.getActiveWindow(); final JDialog dialog = new JDialog(window, "Error " + title + " for " + layerPath, ModalityType.APPLICATION_MODAL); dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); dialog.setLayout(new BorderLayout()); final ToolBar toolBar = new ToolBar(); toolBar.addButtonTitleIcon("default", "Cancel " + title, "table_cancel", () -> { dialog.setVisible(false); cancelAction.accept(this); }); toolBar.addButtonTitleIcon("default", "Save valid records", "table_save", () -> { dialog.setVisible(false); validateRecords(this.invalidRecords, true); successAction.accept(this); }); final TablePanel invalidRecordsTablePanel = newInvalidRecordsTablePanel(); final JLabel message = new JLabel( "<html><b style=\"color:red\">Edit the invalid fields (red background) for the invalid records.</b><br />" + " Valid records will have a green background when edited.</html>"); message.setBorder(BorderFactory.createTitledBorder(layerPath)); final BasePanel panel = new BasePanel(new VerticalLayout(), // toolBar, // message, // invalidRecordsTablePanel // ); panel.setBorder(BorderFactory.createEmptyBorder(3, 6, 3, 6)); if (!this.validRecords.isEmpty()) { final TablePanel validRecordsTablePanel = newValidRecordsTablePanel(); panel.add(validRecordsTablePanel); } dialog.add(panel, BorderLayout.NORTH); final Rectangle screenBounds = SwingUtil.getScreenBounds(); dialog.pack(); dialog.setSize(screenBounds.width - 50, dialog.getPreferredSize().height); SwingUtil.setLocationCentre(screenBounds, dialog); dialog.setVisible(true); SwingUtil.dispose(dialog); }); } private boolean validateField(final LayerRecord record, final int fieldIndex) { final String fieldName = record.getFieldName(fieldIndex); if (Property.hasValue(fieldName)) { try { record.validateField(fieldIndex); final int recordIndex = getInvalidRecordIndex(record); if (recordIndex > -1) { final Map<String, String> fieldErrors = this.invalidRecordErrors.get(recordIndex); fieldErrors.remove(fieldName); } } catch (final ObjectPropertyException e) { final String errorMessage = e.getLocalizedMessage(); addRecordFieldError(record, fieldName, errorMessage); return false; } catch (final Throwable e) { final String errorMessage = e.getLocalizedMessage(); addRecordFieldError(record, fieldName, errorMessage); return false; } } return true; } private void validateRecord(final LayerRecord record, final boolean wasInvalid) { if (this.layer.isLayerRecord(record)) { boolean valid = true; if (!this.layer.isDeleted(record)) { final int fieldCount = record.getFieldCount(); for (int fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) { valid &= validateField(record, fieldIndex); } } if (valid && !wasInvalid) { this.validRecords.add(record); } } else { throw new IllegalArgumentException( "Record must be in layer: " + this.layer.getPath() + "\n" + record); } } private void validateRecords(final Iterable<? extends LayerRecord> records, final boolean wasInvalid) { for (final LayerRecord record : records) { validateRecord(record, wasInvalid); } } }