/** * OrbisGIS is a java GIS application dedicated to research in GIScience. * OrbisGIS is developed by the GIS group of the DECIDE team of the * Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>. * * The GIS group of the DECIDE team is located at : * * Laboratoire Lab-STICC – CNRS UMR 6285 * Equipe DECIDE * UNIVERSITÉ DE BRETAGNE-SUD * Institut Universitaire de Technologie de Vannes * 8, Rue Montaigne - BP 561 56017 Vannes Cedex * * OrbisGIS is distributed under GPL 3 license. * * Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488) * Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285) * * This file is part of OrbisGIS. * * OrbisGIS 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. * * OrbisGIS 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 * OrbisGIS. If not, see <http://www.gnu.org/licenses/>. * * For more information, please consult: <http://www.orbisgis.org/> * or contact directly: * info_at_ orbisgis.org */ package org.orbisgis.tablegui.impl; import java.awt.BorderLayout; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.beans.EventHandler; import java.beans.PropertyChangeListener; import java.sql.*; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import javax.sql.DataSource; import javax.swing.*; import javax.swing.RowSorter.SortKey; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.PopupMenuListener; import javax.swing.event.RowSorterListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.undo.UndoManager; import org.h2gis.utilities.JDBCUtilities; import org.h2gis.utilities.SFSUtilities; import org.h2gis.utilities.TableLocation; import org.orbisgis.commons.progress.SwingWorkerPM; import org.orbisgis.corejdbc.DataManager; import org.orbisgis.corejdbc.MetaData; import org.orbisgis.corejdbc.ReadTable; import org.orbisgis.corejdbc.TableEditEvent; import org.orbisgis.corejdbc.TableEditListener; import org.orbisgis.corejdbc.common.IntegerUnion; import org.orbisgis.commons.progress.NullProgressMonitor; import org.orbisgis.corejdbc.common.LongUnion; import org.orbisgis.coremap.layerModel.ILayer; import org.orbisgis.coremap.layerModel.MapContext; import org.orbisgis.coremap.process.ZoomToSelectedFeatures; import org.orbisgis.editorjdbc.EditableSource; import org.orbisgis.editorjdbc.EditorUndoableEdit; import org.orbisgis.editorjdbc.jobs.CreateSourceFromSelection; import org.orbisgis.mapeditorapi.MapElement; import org.orbisgis.sif.components.actions.ActionCommands; import org.orbisgis.sif.components.actions.DefaultAction; import org.orbisgis.sif.components.filter.DefaultActiveFilter; import org.orbisgis.sif.components.filter.FilterFactoryManager; import org.orbisgis.sif.docking.DockingLocation; import org.orbisgis.sif.docking.DockingPanelParameters; import org.orbisgis.sif.edition.EditableElement; import org.orbisgis.sif.edition.EditableElementException; import org.orbisgis.sif.edition.EditorDockable; import org.orbisgis.sif.edition.EditorManager; import org.orbisgis.tableeditorapi.TableEditableElement; import org.orbisgis.tablegui.icons.TableEditorIcon; import org.orbisgis.tablegui.impl.ext.SourceTable; import org.orbisgis.tablegui.impl.ext.TableEditorActions; import org.orbisgis.tablegui.impl.filters.FieldsContainsFilterFactory; import org.orbisgis.tablegui.impl.filters.TableSelectionFilter; import org.orbisgis.tablegui.impl.filters.WhereSQLFilterFactory; import org.orbisgis.tablegui.impl.jobs.ComputeFieldStatistics; import org.orbisgis.tablegui.impl.jobs.OptimalWidthJob; import org.orbisgis.tablegui.impl.jobs.SearchJob; import org.orbisgis.toolboxeditor.ToolboxWpsClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnap.commons.i18n.I18n; import org.xnap.commons.i18n.I18nFactory; /** * Edit a data source through a grid GUI. * @author Nicolas Fortin */ public class TableEditor extends JPanel implements EditorDockable, SourceTable,TableEditListener { protected final static I18n I18N = I18nFactory.getI18n(TableEditor.class); private static final Logger LOGGER = LoggerFactory.getLogger("gui." + TableEditor.class); private static final int TABLE_SCROLL_PERC = 5; private final UndoManager undoManager = new UndoManager(); private static final long serialVersionUID = 1L; private TableEditableElement tableEditableElement; private DockingPanelParameters dockingPanelParameters; private JTable table; private JScrollPane tableScrollPane; private DataSourceRowSorter tableSorter; private DataSourceTableModel tableModel; private AtomicBoolean initialised = new AtomicBoolean(false); // Property selection change Event trigered by TableEditableElement // is ignored if onUpdateEditableSelection is true private AtomicBoolean onUpdateEditableSelection = new AtomicBoolean(false); private AtomicBoolean filterRunning = new AtomicBoolean(false); private FilterFactoryManager<TableSelectionFilter,DefaultActiveFilter> filterManager = new FilterFactoryManager<>(); private TableRowHeader tableRowHeader; private Point popupCellAdress = new Point(); // Col(x) and row(y) that trigger a popup private Point cellHighlight = new Point(-1,-1); // cell under cursor on right click private PropertyChangeListener editableSelectionListener = EventHandler.create(PropertyChangeListener.class,this, "onEditableSelectionChange"); private PropertyChangeListener filterListener = EventHandler.create(PropertyChangeListener.class,this, "onFilterChange"); private ActionCommands popupActions = new ActionCommands(); private DataSource dataSource; private DataManager dataManager; private MCLayerListener layerListener; private MapContext mapContext; /** Last fetched selected row in selection navigation */ private int currentSelectionNavigation = 0; private EditorManager editorManager; private ExecutorService executorService; private ToolboxWpsClient wpsClient; /** * Constructor * @param element Source to read and edit */ public TableEditor(TableEditableElement element, DataManager dataManager, EditorManager editorManager, ExecutorService executorService, ToolboxWpsClient wpsClient) { super(new BorderLayout()); this.editorManager = editorManager; this.executorService = executorService; layerListener = new MCLayerListener(element); this.dataManager = dataManager; this.dataSource = dataManager.getDataSource(); //Add a listener to the source manager to close the table when //the source is removed this.tableEditableElement = element; dockingPanelParameters = new DockingPanelParameters(); dockingPanelParameters.setTitleIcon(TableEditorIcon.getIcon("table")); dockingPanelParameters.setDefaultDockingLocation(new DockingLocation(DockingLocation.Location .STACKED_ON, "map_editor")); tableScrollPane = new JScrollPane(makeTable()); add(tableScrollPane, BorderLayout.CENTER); updateTitle(); // Fetch MapContext if (editorManager != null) { MapElement mapEditable = MapElement.fetchFirstMapElement(editorManager); if(mapEditable != null) { MapContext mapContext = mapEditable.getMapContext(); registerMapContext(mapContext); } } this.wpsClient = wpsClient; } public void onMenuRefresh() { tableChange(new TableEditEvent(tableEditableElement.getTableReference(), TableModelEvent.ALL_COLUMNS, null, null, TableModelEvent.UPDATE)); } @Override public void tableChange(TableEditEvent event) { if (event.getUndoableEdit() == null && !table.isEditing()) { executorService.execute(new RefreshTableJob(tableModel, tableEditableElement, event, table)); } else { if(event.getUndoableEdit() != null) { undoManager.addEdit(new EditorUndoableEdit(event.getUndoableEdit())); } } for (Action action : getDockActions()) { if (action instanceof ActionAbstractEdition) { ((ActionAbstractEdition) action).onSourceUpdate(); } } } /** * Return the actions available on the top of the table editor * @return */ private List<Action> getDockActions() { List<Action> actions = new LinkedList<>(); actions.add(new DefaultAction(TableEditorActions.A_REFRESH, I18N.tr("Refresh table content"), TableEditorIcon.getIcon("table_refresh"), EventHandler.create(ActionListener.class, this, "onMenuRefresh")) .setLogicalGroup(TableEditorActions.LGROUP_READ)); actions.add(new ActionFilteredRow(tableEditableElement)); actions.add(new DefaultAction(TableEditorActions.A_PREVIOUS_SELECTION, I18N.tr("Previous selection"), I18N.tr("Go to previous selected row"),TableEditorIcon.getIcon("selection-previous"), EventHandler.create(ActionListener.class, this, "onPreviousSelection"), KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK)) .setLogicalGroup(TableEditorActions.LGROUP_READ)); actions.add(new DefaultAction(TableEditorActions.A_NEXT_SELECTION, I18N.tr("Next selection"), I18N.tr("Go to next selected row"),TableEditorIcon.getIcon("selection-next"), EventHandler.create(ActionListener.class, this, "onNextSelection"), KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK)) .setLogicalGroup(TableEditorActions.LGROUP_READ)); // Edition is only available if there is a primary key if(wpsClient != null) { //TODO : actions.add(new ActionAddColumn(tableEditableElement)); actions.add(new ActionAddRow(tableEditableElement, this, wpsClient)); actions.add(new ActionRemoveRow(tableEditableElement, this, wpsClient)); } actions.add(new ActionUndo(tableEditableElement, undoManager)); actions.add(new ActionRedo(tableEditableElement, undoManager)); actions.add(new ActionEdition(tableEditableElement)); return actions; } /** * The editable selection has been updated, * propagate in the table if necessary */ public void onEditableSelectionChange() { if (!onUpdateEditableSelection.getAndSet(true)) { // Convert primary key value into row number try { SortedSet<Integer> modelRows = tableEditableElement.getRowSet().getRowNumberFromRowPk(tableEditableElement.getSelection()); setRowSelection(modelRows, -1); if (!modelRows.isEmpty()) { // Scroll to first selection scrollToRow(modelRows.first() - 1); } } catch (EditableElementException | SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } finally { onUpdateEditableSelection.set(false); } } } /** * The rows have been filtered */ public void onFilterChange(){ if(tableEditableElement.isFiltered()) { onMenuFilterRows(); } else { onMenuClearFilter(); } } /** * @return The first visible row */ private int getViewPosition() { JViewport viewport = tableScrollPane.getViewport(); return table.rowAtPoint(viewport.getViewPosition()); } /** * Return true if the row is visible * @param row * @return */ private boolean isRowVisible(int row) { return table.getVisibleRect().intersects(table.getCellRect(row, 0, true)); } /** * The user want to scroll to the next selected row */ public void onNextSelection() { // Get first shown row int currentRow = currentSelectionNavigation; if(!isRowVisible(currentRow)) { currentRow = getViewPosition(); } // Search next selected row while (currentRow < tableModel.getRowCount()) { if(table.getSelectionModel().isSelectedIndex(++currentRow)) { scrollToRow(currentRow); break; } } } /*** * The user want to scroll to the previous selected row */ public void onPreviousSelection() { // Get first shown row int currentRow = currentSelectionNavigation; if(!isRowVisible(currentRow)) { currentRow = getViewPosition(); } // Search next selected row while (currentRow > 0) { if(table.getSelectionModel().isSelectedIndex(--currentRow)) { scrollToRow(currentRow); break; } } } /** * @param modelRowId Scroll to this model row id */ public void scrollToRow(int modelRowId) { SearchJob.scrollToRow(modelRowId, table); currentSelectionNavigation = modelRowId; } /** * The popup is destroyed, the cell border need to be removed */ public void onPopupBecomeInvisible() { cellHighlight.setLocation(-1, -1); } /** * The popup is shown, the cell border need to be set */ public void onPopupBecomeVisible() { cellHighlight.setLocation(popupCellAdress); } /** * Create the filter panel * @return */ private JComponent makeFilterManager() { JPanel filterComp = filterManager.makeFilterPanel(false); filterManager.setUserCanRemoveFilter(false); FieldsContainsFilterFactory factory = new FieldsContainsFilterFactory(table); filterManager.registerFilterFactory(factory); // SQL filter is only available if there is a primary key try(Connection connection = dataSource.getConnection()) { int idPk = JDBCUtilities.getIntegerPrimaryKey(connection, tableEditableElement.getTableReference()); if(idPk > 0) { filterManager.registerFilterFactory(new WhereSQLFilterFactory()); } } catch (SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } filterManager.addFilter(factory.getDefaultFilterValue()); filterManager.getEventFilterChange().addListener(this, EventHandler.create(FilterFactoryManager.FilterChangeListener.class, this, "onApplySelectionFilter")); return filterComp; } /** * Apply the active search filters */ public void onApplySelectionFilter() { List<TableSelectionFilter> filters = filterManager.getFilters(); if(!filterRunning.getAndSet(true)) { executorService.execute(new SearchJob(filters.get(0), table, tableEditableElement, filterRunning)); } else { LOGGER.info(I18N.tr("Searching request is already launched. Please wait a moment, or cancel it.")); } } /** * Reload filter GUI components */ private void reloadFilters() { LOGGER.debug("Reload filter"); DefaultActiveFilter currentFilter = filterManager.getFilterValues().iterator().next(); filterManager.clearFilters(); filterManager.addFilter(currentFilter); } /** * Create the table and its actions * @return */ private JComponent makeTable() { table = new JTable(); table.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF); table.setAutoCreateColumnsFromModel(false); table.addMouseListener(EventHandler.create(MouseListener.class, this, "onMouseActionOnTableCells", "")); table.getTableHeader().addMouseListener(EventHandler.create(MouseListener.class, this, "onMouseActionOnTableHeader", "")); table.getSelectionModel().setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.getTableHeader().setReorderingAllowed(false); table.setColumnSelectionAllowed(true); //table.setPreferredScrollableViewportSize(new Dimension(500, 70)); table.setFillsViewportHeight(true); table.setUpdateSelectionOnSort(true); table.setDragEnabled(true); table.setBackground(this.getBackground()); return table; } /** * Right click on column header. */ public void onMouseActionOnTableHeader(MouseEvent e) { //Does this action correspond to a popup request if (e.isPopupTrigger()) { int col = table.columnAtPoint(e.getPoint()); popupCellAdress.setLocation(col,-1); JPopupMenu menu = makeTableHeaderPopup(col); menu.show(e.getComponent(), e.getX(), e.getY()); } } /** * Right click on a table cell. */ public void onMouseActionOnTableCells(MouseEvent e) { //Does this action correspond to a popup request if (e.isPopupTrigger()) { int row = table.rowAtPoint(e.getPoint()); int col = table.columnAtPoint(e.getPoint()); popupCellAdress.setLocation(col,row); JPopupMenu menu = makeTableCellPopup(); menu.addPopupMenuListener(EventHandler.create(PopupMenuListener.class, this, "onPopupBecomeInvisible",null,"popupMenuWillBecomeInvisible")); menu.addPopupMenuListener(EventHandler.create(PopupMenuListener.class, this, "onPopupBecomeVisible",null,"popupMenuWillBecomeVisible")); menu.show(e.getComponent(), e.getX(), e.getY()); } } /** * Create popup menu when the user click on a cell * @return */ private JPopupMenu makeTableCellPopup() { JPopupMenu pop = new JPopupMenu(); boolean hasSelectedRows = table.getSelectedRowCount()>0; if(hasSelectedRows && !tableSorter.isFiltered()) { JMenuItem addRowFilter = new JMenuItem(I18N.tr("Filter selected rows"), TableEditorIcon.getIcon("row_filter")); addRowFilter.setToolTipText(I18N.tr("Show only the selected rows")); addRowFilter.addActionListener( EventHandler.create(ActionListener.class, this,"onMenuFilterRows")); pop.add(addRowFilter); } if(tableSorter.isFiltered()) { JMenuItem removeRowFilter = new JMenuItem( I18N.tr("Clear row filter"), TableEditorIcon.getIcon("row_filter_remove")); removeRowFilter.setToolTipText(I18N.tr("Show all rows")); removeRowFilter.addActionListener( EventHandler.create(ActionListener.class, this,"onMenuClearFilter")); pop.add(removeRowFilter); } if(hasSelectedRows || tableSorter.isFiltered()) { pop.addSeparator(); } if(hasSelectedRows) { JMenuItem createDataSourceSelection = new JMenuItem( I18N.tr("Create datasource from selection"), TableEditorIcon.getIcon("table_go")); createDataSourceSelection.setToolTipText( I18N.tr("Create a datasource from the current selection")); createDataSourceSelection.addActionListener( EventHandler.create(ActionListener.class, this, "onCreateDataSourceFromSelection")); pop.add(createDataSourceSelection); JMenuItem deselectAll = new JMenuItem( I18N.tr("Clear selection"), TableEditorIcon.getIcon("edit-clear")); deselectAll.setToolTipText(I18N.tr("Deselect all lines")); deselectAll.addActionListener( EventHandler.create(ActionListener.class, this, "onMenuClearSelection")); pop.add(deselectAll); if (isDataOnShownMapContext()) { JMenuItem zoomToSelection = new JMenuItem( I18N.tr("Zoom to selection"), TableEditorIcon.getIcon("zoom_selected")); zoomToSelection.setToolTipText(I18N.tr("In the map editor, zoom to the selected rows")); zoomToSelection.addActionListener( EventHandler.create(ActionListener.class, this, "onMenuZoomToSelection")); pop.add(zoomToSelection); } JMenuItem inverseSelection = new JMenuItem( I18N.tr("Reverse selection"), TableEditorIcon.getIcon("reverse_selection")); inverseSelection.setToolTipText(I18N.tr("Reverse the current selection")); inverseSelection.addActionListener( EventHandler.create(ActionListener.class, this, "onMenuReverseSelection")); pop.add(inverseSelection); } JMenuItem findSameCells = new JMenuItem( I18N.tr("Select same cell"),TableEditorIcon.getIcon("selectsame_row")); findSameCells.setToolTipText(I18N.tr("Select all rows that match this cell value")); findSameCells.addActionListener( EventHandler.create(ActionListener.class, this,"onMenuSelectSameCellValue")); pop.add(findSameCells); popupActions.copyEnabledActions(pop); return pop; } /** * Used by the function "Zoom to selection" * This menu is shown only if the current data is loaded and shown in the toc */ private boolean isDataOnShownMapContext() { TableLocation editorTable = TableLocation.parse(tableEditableElement.getTableReference()); if (editorManager != null) { MapElement mapEditable = MapElement.fetchFirstMapElement(editorManager); if(mapEditable != null) { MapContext mapContext = mapEditable.getMapContext(); for (ILayer layer : mapContext.getLayers()) { TableLocation layerTable = TableLocation.parse(layer.getTableReference()); if (layer.isVisible()) { if (editorTable.getSchema().equals(layerTable.getSchema()) && editorTable.getTable().equals(layerTable.getTable())) { return true; } } } } } return false; } /** * Zoom on the current selection */ public void onMenuZoomToSelection() { if(table.getSelectionModel().isSelectionEmpty()) { return; } //Retrieve the MapContext MapContext mapContext=null; for(EditableElement editable : editorManager.getEditableElements()) { if(editable instanceof MapElement) { MapElement mapEditable = (MapElement)editable; mapContext = mapEditable.getMapContext(); break; } } if(mapContext==null) { //Software error, useless to translate LOGGER.error("MapContext lost between popup creation and click"); return; } executorService.execute(new ZoomToSelectedFeatures(dataManager, tableEditableElement .getTableReference(), tableEditableElement.getSelection(), mapContext)); } /** * Show all rows of the data source (remove the filter) */ public void onMenuClearFilter() { tableSorter.setRowsFilter(null); tableEditableElement.setFiltered(false); } /** * Invert the current table selection */ public void onMenuReverseSelection() { IntegerUnion invertedSelection = new IntegerUnion(); for(int viewId = 0; viewId<table.getRowCount();viewId++) { if(!table.isRowSelected(viewId)) { invertedSelection.add(viewId); } } setViewRowSelection(invertedSelection); } /** * The user can export the selected rows into a new datasource */ public void onCreateDataSourceFromSelection() { // If there is a nonempty selection, then ask the user to name it. if (!tableEditableElement.getSelection().isEmpty()) { try { String newName = CreateSourceFromSelection.showNewNameDialog(this, dataSource, tableEditableElement.getTableReference()); // If newName is not null, then the user clicked OK and entered // a valid name. if (newName != null) { executorService.execute(new CreateSourceFromSelection(dataSource, tableEditableElement .getSelection(), tableEditableElement.getTableReference(), newName)); } } catch (SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } } } /** * Select all rows that have the same value of the selected cell */ public void onMenuSelectSameCellValue() { int viewColId = popupCellAdress.x; int viewRowId = popupCellAdress.y; int colId = table.convertColumnIndexToModel(viewColId); int rowId = table.convertRowIndexToModel(viewRowId); //Build the appropriate search filter Object value = tableModel.getValueAt(rowId, colId); DefaultActiveFilter filter = null; if(value==null){ filter = new FieldsContainsFilterFactory. FilterParameters(colId, null, true, true); } else{ filter = new FieldsContainsFilterFactory. FilterParameters(colId, value.toString(), true, true); } //Clear current filter filterManager.clearFilters(); //Add the find filter filterManager.addFilter(filter); //Trigger the filter job onApplySelectionFilter(); } /** * Clear the table selection */ public void onMenuClearSelection() { table.clearSelection(); } /** * Show only selected rows */ public void onMenuFilterRows() { IntegerUnion selectedModelIndex = getTableModelSelection(0); tableSorter.setRowsFilter(selectedModelIndex); } /** * Create the popup menu of the table header * @param col * @return */ private JPopupMenu makeTableHeaderPopup(Integer col) { JPopupMenu pop = new JPopupMenu(); //Optimal width JMenuItem optimalWidth = new JMenuItem(I18N.tr("Optimal width"), TableEditorIcon.getIcon("text_letterspacing") ); optimalWidth.addActionListener( EventHandler.create(ActionListener.class,this, "onMenuOptimalWidth")); pop.add(optimalWidth); // Additional functions for specific columns boolean isGeometryField = false; try(Connection connection = dataSource.getConnection()) { List<String> geomFields = SFSUtilities.getGeometryFields(connection, TableLocation.parse(tableEditableElement.getTableReference())); ResultSetMetaData meta = tableEditableElement.getRowSet().getMetaData(); for(String geomField : geomFields) { int gIndex = JDBCUtilities.getFieldIndex(meta, geomField); if(col.equals(gIndex - 1)) { isGeometryField = true; } } } catch (SQLException | EditableElementException ex ){ LOGGER.error(ex.getLocalizedMessage(), ex); } if (!isGeometryField) { pop.addSeparator(); //Sort Ascending JMenuItem sortAscending = new JMenuItem(I18N.tr("Sort ascending"), UIManager.getIcon("Table.ascendingSortIcon") ); sortAscending.addActionListener( EventHandler.create(ActionListener.class,this, "onMenuSortAscending")); pop.add(sortAscending); //Sort Descending JMenuItem sortDescending = new JMenuItem(I18N.tr("Sort descending"), UIManager.getIcon("Table.descendingSortIcon") ); sortDescending.addActionListener( EventHandler.create(ActionListener.class,this, "onMenuSortDescending")); pop.add(sortDescending); //No sort JMenuItem noSort = new JMenuItem(I18N.tr("No sort"), TableEditorIcon.getIcon("table_refresh") ); noSort.addActionListener( EventHandler.create(ActionListener.class,this, "onMenuNoSort")); pop.add(noSort); } pop.addSeparator(); //Get Field information JMenuItem showFieldInformation = new JMenuItem(I18N.tr("Show column information"), TableEditorIcon.getIcon("information") ); showFieldInformation.addActionListener( EventHandler.create(ActionListener.class, this, "onMenuShowInformation") ); pop.add(showFieldInformation); if(isNumeric(col)) { //Get Statistics String text = I18N.tr("Show column statistics"); if(table.getSelectedRowCount()>0) { text = I18N.tr("Show column selection statistics"); } else if(tableSorter.isFiltered()) { text = I18N.tr("Show filtered column statistics"); } JMenuItem showStats = new JMenuItem(text, TableEditorIcon.getIcon("statistics") ); showStats.addActionListener( EventHandler.create(ActionListener.class,this, "onMenuShowStatistics")); pop.add(showStats); } popupActions.copyEnabledActions(pop); return pop; } /** * The user disable table sort */ public void onMenuNoSort() { tableSorter.setSortKeys(null); } /** * Ascending sort */ public void onMenuSortAscending() { tableSorter.setSortKey(new SortKey(popupCellAdress.x, SortOrder.ASCENDING)); } /** * Descending sort */ public void onMenuSortDescending() { tableSorter.setSortKey(new SortKey(popupCellAdress.x, SortOrder.DESCENDING)); } /** * Show the selected field information */ public void onMenuShowInformation() { int col = popupCellAdress.x + 1; try(Connection connection = dataSource.getConnection()) { DatabaseMetaData meta = connection.getMetaData(); LOGGER.info(MetaData.getColumnInformations(meta, tableEditableElement.getTableReference(), col)); } catch( SQLException ex) { LOGGER.error(ex.getLocalizedMessage(),ex); } } /** * @param zeroBaseDiff JTable selection is 0 based. Set 1 in order to get a 1 based row identifier selection. * @return Select row id */ public IntegerUnion getTableModelSelection(int zeroBaseDiff) { IntegerUnion selectionModelRowId = new IntegerUnion(); for (int viewRowId : table.getSelectedRows()) { selectionModelRowId.add(tableSorter.convertRowIndexToModel(viewRowId) + zeroBaseDiff); } return selectionModelRowId; } /** * Compute and show the selected field statistics */ public void onMenuShowStatistics() { //Compute row id selection Set<Integer> selectionModelRowId = getTableModelSelection(1); if (selectionModelRowId.isEmpty() && tableSorter.isFiltered()) { selectionModelRowId.addAll(tableSorter.getViewToModelIndex()); } executorService.execute(new ComputeFieldStatistics(selectionModelRowId, dataSource, popupCellAdress .x, tableEditableElement.getTableReference())); } /** * Compute the optimal width for this column */ public void onMenuOptimalWidth() { executorService.execute(new OptimalWidthJob(table, popupCellAdress.x)); } /** * Return the editable document * @return */ @Override public EditableSource getTableEditableElement() { return tableEditableElement; } @Override public boolean match(EditableElement editableElement) { return editableElement instanceof MapElement || editableElement instanceof TableEditableElement; } /** * Link row selection with toc layer's selection * Link toc layer selection with table selection * @param mc MapContext instance */ private void registerMapContext(MapContext mc) { if(mapContext != null) { mapContext.getLayerModel().removeLayerListenerRecursively(layerListener); } mapContext = mc; mc.getLayerModel().addLayerListenerRecursively(layerListener); } @Override public void addNotify() { super.addNotify(); if(!initialised.getAndSet(true)) { executorService.execute(new OpenEditableElement(this, tableEditableElement)); } } private void quickAutoResize() { autoResizeColWidth(Math.min(5, tableModel.getRowCount())); } /** * When the editable element is open, * the data model of the table can be set. * Called only once. */ private void readDataSource() { tableModel = new DataSourceTableModel(tableEditableElement); tableEditableElement.getDataManager().addTableEditListener(tableEditableElement.getTableReference(), this, false); tableModel.addTableModelListener(new FieldResetListener(this)); table.setModel(tableModel); updateTableColumnModel(); quickAutoResize(); tableSorter = new DataSourceRowSorter(tableModel, dataSource); tableSorter.addRowSorterListener( EventHandler.create(RowSorterListener.class,this, "onShownRowsChanged")); tableSorter.setExecutorService(executorService); table.setRowSorter(tableSorter); //Set the row count at left tableRowHeader = new TableRowHeader(table); tableScrollPane.setRowHeaderView(tableRowHeader); //Apply the selection try { setRowSelection(tableEditableElement.getRowSet().getRowNumberFromRowPk(tableEditableElement .getSelection()), -1); //Apply the filtered row action if(tableEditableElement.isFiltered()){ IntegerUnion selectedModelIndex = getTableModelSelection(0); tableSorter.setRowsFilter(selectedModelIndex); } if (!table.getSelectionModel().isSelectionEmpty()) { scrollToRow(table.getSelectionModel().getMinSelectionIndex()); } } catch (EditableElementException |SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } table.getSelectionModel().addListSelectionListener( EventHandler.create(ListSelectionListener.class,this, "onTableSelectionChange","")); add(makeFilterManager(),BorderLayout.SOUTH); //Close the editable element on window close dockingPanelParameters.addPropertyChangeListener( DockingPanelParameters.PROP_VISIBLE, EventHandler.create(PropertyChangeListener.class, this,"onChangeVisibility","newValue")); updateTitle(); // Add a selection listener on the editable element tableEditableElement.addPropertyChangeListener(TableEditableElement.PROP_SELECTION, editableSelectionListener); // Add a filter listener on the editable element tableEditableElement.addPropertyChangeListener(TableEditableElement.PROP_FILTERED, filterListener); dockingPanelParameters.setDockActions(getDockActions()); initPopupActions(); tableScrollPane.getVerticalScrollBar().setBlockIncrement((int)(table.getHeight() / (TABLE_SCROLL_PERC / 100.))); } private void initPopupActions() { if(tableEditableElement.isEditable()) { popupActions.addAction(new ActionRemoveColumn(this, wpsClient)); } } /** * Frame visibility state change * @param visible */ public void onChangeVisibility(boolean visible) { if(!visible) { dataManager.removeTableEditListener(tableEditableElement.getTableReference(), this); if(mapContext != null) { mapContext.getLayerModel().removeLayerListenerRecursively(layerListener); } for(Action action : dockingPanelParameters.getDockActions()) { if(action instanceof ActionDispose){ try { ((ActionDispose) action).dispose(); }catch (Exception ex) { LOGGER.error(ex.getLocalizedMessage(),ex); } } } try { LOGGER.debug("Close table "+dockingPanelParameters.getTitle()); tableEditableElement.close(new NullProgressMonitor()); tableEditableElement.removePropertyChangeListener(editableSelectionListener); tableEditableElement.removePropertyChangeListener(filterListener); } catch (UnsupportedOperationException | EditableElementException ex) { LOGGER.error(ex.getLocalizedMessage(),ex); } } } /** * Convert index from model to view then update the table selection * @param modelSelection ModelIndex selection * @param zeroBasedDiff JTable selection is 0 based, you can offset the selection row identifier by defining -1 if your selection is 1 based */ private void setRowSelection(Set<Integer> modelSelection, int zeroBasedDiff) { Set<Integer> newSelection; if(tableSorter.isFiltered() || !tableSorter.getSortKeys().isEmpty()) { newSelection = new IntegerUnion(); for(int modelId : modelSelection) { modelId += zeroBasedDiff; int viewRowId = table.convertRowIndexToView(modelId); if(viewRowId!=-1) { newSelection.add(viewRowId); } } } else { newSelection = new IntegerUnion(); for(int modelId : modelSelection) { newSelection.add(modelId + zeroBasedDiff); } } setViewRowSelection(newSelection); } /** * Update the table selection * @param viewSelection View index selection */ private void setViewRowSelection(Set<Integer> viewSelection) { // Integer union is able to compute range of integer from a set of integer Iterator<Integer> intervals = new IntegerUnion(viewSelection).getValueRanges().iterator(); final int maxRow = table.getRowCount(); try { table.getSelectionModel().setValueIsAdjusting(true); table.clearSelection(); while(intervals.hasNext()) { // If the DataSource here and in other editors is not the same (uncommitted changes) // Then the selected row index may not be the same and can be out of range. // The check is done here. int begin = intervals.next(); int end = Math.min(intervals.next(),maxRow - 1); if(begin < maxRow) { table.addRowSelectionInterval(begin, end); } } }finally { table.getSelectionModel().setValueIsAdjusting(false); } } /** * The model or the sorted have updated the table */ public void onShownRowsChanged() { updateTitle(); tableRowHeader.tableChanged(); } /** * Table selection change * @param evt Selection event, used to test if the selection is final */ public void onTableSelectionChange(ListSelectionEvent evt) { if (!evt.getValueIsAdjusting()) { updateTitle(); if (!onUpdateEditableSelection.getAndSet(true)) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { updateEditableSelection(); } finally { onUpdateEditableSelection.set(false); } } }); } } } private void updateEditableSelection() { try { tableEditableElement.setSelection(ReadTable.getRowPkFromRowNumber(tableEditableElement.getRowSet(), getTableModelSelection(1))); // Update layer selection if (mapContext != null) { TableLocation editorTable = TableLocation.parse(tableEditableElement.getTableReference()); // Search layers with same table identifier ILayer[] layers = mapContext.getLayers(); for (ILayer layer : layers) { if (!layer.getTableReference().isEmpty()) { TableLocation layerTable = TableLocation.parse(layer.getTableReference()); if (editorTable.equals(layerTable)) { layer.setSelection(tableEditableElement.getSelection()); } } } } } catch (EditableElementException | SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } finally { onUpdateEditableSelection.set(false); } } /** * Update the title label. */ private void updateTitle() { String sourceName = tableEditableElement.getTableReference(); int tableSelectedRowCount = table.getSelectedRowCount(); int tableRowCount = table.getRowCount(); // Message is different if the table is filtered if(tableSorter==null || !tableSorter.isFiltered()) { dockingPanelParameters.setTitle( I18N.tr("Table Editor of {0} {1}/{2}", sourceName,tableSelectedRowCount,tableRowCount)); }else{ dockingPanelParameters.setTitle( I18N.tr("Table Editor of {0} (Filtered) {1}/{2}", sourceName,tableSelectedRowCount,tableRowCount)); } } private void resetRenderers() { for (int i = 0; i < tableModel.getColumnCount(); i++) { TableColumn col = table.getColumnModel().getColumn(i); if (isNumeric(i)) { col.setCellRenderer(new TableNumberColumnRenderer(table,cellHighlight)); } else { col.setCellRenderer(new TableDefaultColumnRenderer(table,tableModel.getColumnClass(i),cellHighlight)); } } } private void autoResizeColWidth(int rowsToCheck) { TableColumnModel colModel = table.getColumnModel(); int maxWidth = 200; for (int i = 0; i < colModel.getColumnCount(); i++) { TableColumn col = colModel.getColumn(i); int colWidth = OptimalWidthJob.getColumnOptimalWidth(table, rowsToCheck, maxWidth, i, new NullProgressMonitor()); col.setPreferredWidth(colWidth); } resetRenderers(); } /** * Sync the table column model with the DataSource */ private void updateTableColumnModel() { TableColumnModel colModel = new DefaultTableColumnModel(); for (int i = 0; i < tableModel.getColumnCount(); i++) { TableColumn col = new TableColumn(i); String columnName = tableModel.getColumnName(i); col.setHeaderValue(columnName); TableCellRenderer headerRenderer = col.getHeaderRenderer(); if(!(headerRenderer instanceof TableEditorHeaderRenderer)) { TableEditorHeaderRenderer newRenderer = new TableEditorHeaderRenderer(table); try { newRenderer.setKey(isPrimaryKey(columnName)); } catch (SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } col.setHeaderRenderer(newRenderer); } colModel.addColumn(col); } table.setColumnModel(colModel); } private boolean isPrimaryKey(String columnName) throws SQLException { TableLocation tableLocation = TableLocation.parse(tableEditableElement.getTableReference()); try(Connection connection = dataSource.getConnection(); ResultSet rs = connection.getMetaData().getPrimaryKeys(tableLocation.getCatalog(null), tableLocation.getSchema(null), tableLocation.getTable())) { while (rs.next()) { // If the schema is not specified, public must be the schema if (!tableLocation.getSchema().isEmpty() || "public".equalsIgnoreCase(rs.getString("TABLE_SCHEM"))) { if (columnName.equals(rs.getString("COLUMN_NAME"))) { return true; } } } } return false; } /** * @param column Column index * @return True if the field type is numeric */ private boolean isNumeric(int column) { try { int columnType = tableModel.getColumnType(column); switch (columnType) { case Types.FLOAT: case Types.DOUBLE: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.DECIMAL: case Types.NUMERIC: return true; default: return false; } }catch (SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); return false; } } @Override public EditableElement getEditableElement() { return tableEditableElement; } @Override public void setEditableElement(EditableElement editableElement) { if(editableElement instanceof MapElement) { mapContext = ((MapElement) editableElement).getMapContext(); registerMapContext(mapContext); } } @Override public DockingPanelParameters getDockingParameters() { return dockingPanelParameters; } @Override public JComponent getComponent() { return this; } private static class OpenEditableElement extends SwingWorkerPM { private TableEditor tableEditor; private TableEditableElement tableEditableElement; private OpenEditableElement(TableEditor tableEditor, TableEditableElement el) { this.tableEditor = tableEditor; this.tableEditableElement = el; setTaskName(I18N.tr("Open the table {0}", tableEditableElement.getTableReference())); } @Override protected Object doInBackground() throws Exception { try { try { if (tableEditableElement.isOpen()) { tableEditableElement.close(this.getProgressMonitor()); } tableEditableElement.open(this.getProgressMonitor()); } catch (UnsupportedOperationException | EditableElementException ex) { LOGGER.error(I18N.tr("Error while loading the table editor"), ex); } } finally { tableEditor.initialised.set(true); } return null; } @Override protected void done() { tableEditor.readDataSource(); } } @Override public JTable getTable() { return table; } @Override public Point getPopupCellAdress() { return new Point(popupCellAdress); } private static class FieldResetListener implements TableModelListener { private final TableEditor tableEditor; private FieldResetListener(TableEditor tableEditor) { this.tableEditor = tableEditor; } @Override public void tableChanged(TableModelEvent tableModelEvent) { if (tableModelEvent.getFirstRow() == TableModelEvent.HEADER_ROW) { tableEditor.updateTableColumnModel(); tableEditor.reloadFilters(); tableEditor.resetRenderers(); } } } /** * Close TableEditor in Swing Thread */ private static class CloseTableEditor implements Runnable { private final TableEditor tableEditor; private CloseTableEditor(TableEditor tableEditor) { this.tableEditor = tableEditor; } @Override public void run() { tableEditor.dockingPanelParameters.setVisible(false); } } private class RefreshTableJob extends SwingWorkerPM<Boolean, Boolean> { private DataSourceTableModel model; private JTable tableComp; private TableEditableElement table; private List<TableModelEvent> evts = new ArrayList<>(); private TableEditEvent event; private RefreshTableJob(DataSourceTableModel model, TableEditableElement table, TableEditEvent event, JTable tableComp) { this.model = model; this.table = table; this.event = event; this.tableComp = tableComp; setTaskName(I18N.tr("Refresh table content")); } @Override protected void done() { model.setLastFetchRowCountTime(0); // Swing Thread // Send columns delete/insert/update events Rectangle rect = tableComp.getVisibleRect(); int firstVisibleRow = tableComp.rowAtPoint(rect.getLocation()); int lastVisibleRow = tableComp.rowAtPoint(new Point(rect.x, rect.y + rect.height - 1)); if(firstVisibleRow < lastVisibleRow && firstVisibleRow >= 0 && lastVisibleRow <= tableComp.getRowCount()) { IntegerUnion rowsToClean = new IntegerUnion(); for(int viewRow = firstVisibleRow; viewRow <= lastVisibleRow; viewRow++) { rowsToClean.add(tableComp.convertRowIndexToModel(viewRow) + 1); } try { table.getRowSet().refreshRows(rowsToClean); // Update rendered rows Iterator<Integer> intervals = rowsToClean.getValueRanges().iterator(); while(intervals.hasNext()) { int start = intervals.next(); int end = intervals.next(); model.fireTableRowsUpdated(start - 1, end - 1); } } catch (SQLException | EditableElementException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } } if(evts.isEmpty()) { // Refresh shown data model.fireTableDataChanged(); } else { for (TableModelEvent evt : evts) { model.fireTableChanged(evt); } } } @Override protected Boolean doInBackground() throws Exception { if(event.getColumn() == TableModelEvent.ALL_COLUMNS || event.getFirstRowPK() == null || event.getLastRowPK() == null) { List<String> columnTypes = new ArrayList<>(); List<String> columnNames = new ArrayList<>(); try { try { if(!table.isOpen()) { table.open(getProgressMonitor()); } ResultSetMetaData meta = table.getRowSet().getMetaData(); for (int col = 1; col < meta.getColumnCount(); col++) { columnNames.add(meta.getColumnName(col)); columnTypes.add(meta.getColumnTypeName(col)); } } catch (SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } // The row count may have changed, reset the rowset table.getRowSet().execute(); try { ResultSetMetaData meta = table.getRowSet().getMetaData(); for (int col = 1; col < meta.getColumnCount(); col++) { if (col <= columnNames.size()) { if (!columnNames.get(col - 1).equals(meta.getColumnName(col)) || !columnTypes.get(col - 1).equals(meta.getColumnTypeName(col))) { evts.add(new TableModelEvent(model, TableModelEvent.HEADER_ROW, TableModelEvent.HEADER_ROW, col - 1, TableModelEvent.UPDATE)); } //columnTypes.add(meta.getColumnTypeName(col + offset)); } else { //New column evts.add(new TableModelEvent(model, TableModelEvent.HEADER_ROW, TableModelEvent.HEADER_ROW, col - 1, TableModelEvent.INSERT)); } } // Deleted columns if (meta.getColumnCount() < columnNames.size()) { for (int insertId = meta.getColumnCount(); insertId <= columnNames.size(); insertId++) { evts.add(new TableModelEvent(model, TableModelEvent.HEADER_ROW, TableModelEvent.HEADER_ROW, meta.getColumnCount() - 1, TableModelEvent.DELETE)); } } } catch (SQLException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } } catch (EditableElementException ex) { LOGGER.error(ex.getLocalizedMessage(), ex); } } else { // Simple row event IntegerUnion updatedRows = new IntegerUnion(table.getRowSet().getRowNumberFromRowPk(new LongUnion(event.getFirstRowPK(), event.getLastRowPK()))); Iterator<Integer> intervals = updatedRows.getValueRanges().iterator(); while (intervals.hasNext()) { int firstRow = intervals.next(); int lastRow = intervals.next(); evts.add(new TableModelEvent(model, firstRow - 1, lastRow - 1, event.getColumn(), event.getType() )); } // Refresh rowset cache table.getRowSet().refreshRows(new TreeSet<>(updatedRows)); } return true; } } /** * Return the DataSourceRowSorter to filter or short the data of the table * @return */ public DataSourceRowSorter getDataSourceRowSorter() { return tableSorter; } }