-
Bug
-
Resolution: Unresolved
-
P3
-
8, 11, 17, 19, 20
-
generic
-
generic
A DESCRIPTION OF THE PROBLEM :
The method DefaultRowSorter.getComparator(int) performs bound checks using the wrong bounds. It calls checkCoumn which uses getModelWrapper().getColumnCount() to checks the upper bound instead of checking against comparators.length
The mismatch between the current column count and the size of the comparators array is caused occurs because the AbstractTableModel.fireTableChanged(TableModelEvent) notifies the listeners in reverse order.
This means I see two issues: The wrong kind of bound check and the reverse order of TableModelListener notifications. If the TableModelListeners would be notified in registration order, then the comparators would already be reset by DefaultRowSorter.allChanged() and therefore the getComparator(int) would always return null.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Create a DefaultTableModel
2) Create a JTable using the TableModel
3) Enable AutoCreateRowSorter
4) Set a Comparator to any column
5) Add a TableModelListener that resets the RowFilter for TableStructureChanged events
6) Increase the column count. This will fire a TableStructureChanged event
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No error occurs.
The TableStructureChanged event should reset the RowFilter (including the comparators), i.e. the private allChanged() should be called. A custom TableModelListener that resets the RowFilter must not fail.
ACTUAL -
An ArrayIndexOutOfBoundsException is thrown, as the TableModel has more columns than the internal comparators array. The comparators array is not null and its length does not match the current column count. The private DefaultRowSorter.allChanged() is not yet called, as the TableModelListeners are called in reverse order. (See javax.swing.table.AbstractTableModel.fireTableChanged(TableModelEvent))
The method DefaultRowSorter.allChanged() is called by DefaultRowSorter.modelStructureChanged() which is called by the JTable.tableChanged(TableModelEvent) implementing the TableModelListener.
---------- BEGIN SOURCE ----------
import java.awt.BorderLayout;
import java.util.Comparator;
import javax.swing.DefaultRowSorter;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.TableModelEvent;
import javax.swing.table.DefaultTableModel;
public class DefaultRowSorterBug
{
/**
* @param args ignored
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
/**
* NOTE: The run() method here contains only boilerplate code to bring up a visible dialog.
*/
@Override
public void run() {
final JDialog dialog = new JDialog();
JComponent content = createContent();
dialog.getContentPane().add(content, BorderLayout.CENTER);
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dialog.pack();
dialog.setLocationRelativeTo(null);
dialog.setVisible(true);
}
/**
* Creates the JTable and demonstrates the issue.
* @return the JTable to display.
*/
protected JComponent createContent() {
// (1) Create a TableModel that derives from AbstractTableModel
var model = new DefaultTableModel(new String[][] { { "One", "Orange" }, { "Two", "Green" } }, new String[] { "First", "Second" });
// (2) Create a JTable with that model
JTable table = new JTable(model);
// (3) Enable AutoCreateRowSorter (or set any RowSorter that derives from DefaultRowSorter)
table.setAutoCreateRowSorter(true);
// (4) Set at least one Comparator to any column
((DefaultRowSorter<?, ?>) table.getRowSorter()).setComparator(0, Comparator.naturalOrder());
// (5) Add a TableModelListener that resets the RowFilter on TableStructureChanged events
model.addTableModelListener(e -> {
if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
DefaultRowSorter<?, ?> rowSorter = (DefaultRowSorter<?, ?>) table.getRowSorter();
// The following line will create an java.lang.ArrayIndexOutOfBoundsException in DefaultRowSorter.getComparator
// The method getComparator already calls checkColumn to check the parameter value, but that check is done using the current value of the TableModel
// which is already updated to be three. But the Arrays of comparators wan't updated yet.
// There is no issue if the column count is reduced, but the issue occurs when the column count is increased.
rowSorter.setRowFilter(null);
}
});
// (6) At a later point in time -> increase the column count
SwingUtilities.invokeLater(() -> model.setColumnCount(3));
// NOTE: This is a very simple example. A read-world example may have a custom TableModel that adjusts its own structure due to some external events.
return table;
}
});
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Reset the RowSorter before increasing the columns of the TableModel.
For this workaround to be applicable the code that adjust the TableModel must also have access to the (or all) JTables that display the Model. If the TableModel is independent from any view and adjusts its own column count, then this workaround cannot be used.
Another workaround could be to "fix the issue" in the derived TableModel implementation and override javax.swing.table.AbstractTableModel.fireTableChanged(TableModelEvent) and notify all listeners in registration order.
FREQUENCY : always
The method DefaultRowSorter.getComparator(int) performs bound checks using the wrong bounds. It calls checkCoumn which uses getModelWrapper().getColumnCount() to checks the upper bound instead of checking against comparators.length
The mismatch between the current column count and the size of the comparators array is caused occurs because the AbstractTableModel.fireTableChanged(TableModelEvent) notifies the listeners in reverse order.
This means I see two issues: The wrong kind of bound check and the reverse order of TableModelListener notifications. If the TableModelListeners would be notified in registration order, then the comparators would already be reset by DefaultRowSorter.allChanged() and therefore the getComparator(int) would always return null.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Create a DefaultTableModel
2) Create a JTable using the TableModel
3) Enable AutoCreateRowSorter
4) Set a Comparator to any column
5) Add a TableModelListener that resets the RowFilter for TableStructureChanged events
6) Increase the column count. This will fire a TableStructureChanged event
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No error occurs.
The TableStructureChanged event should reset the RowFilter (including the comparators), i.e. the private allChanged() should be called. A custom TableModelListener that resets the RowFilter must not fail.
ACTUAL -
An ArrayIndexOutOfBoundsException is thrown, as the TableModel has more columns than the internal comparators array. The comparators array is not null and its length does not match the current column count. The private DefaultRowSorter.allChanged() is not yet called, as the TableModelListeners are called in reverse order. (See javax.swing.table.AbstractTableModel.fireTableChanged(TableModelEvent))
The method DefaultRowSorter.allChanged() is called by DefaultRowSorter.modelStructureChanged() which is called by the JTable.tableChanged(TableModelEvent) implementing the TableModelListener.
---------- BEGIN SOURCE ----------
import java.awt.BorderLayout;
import java.util.Comparator;
import javax.swing.DefaultRowSorter;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.TableModelEvent;
import javax.swing.table.DefaultTableModel;
public class DefaultRowSorterBug
{
/**
* @param args ignored
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
/**
* NOTE: The run() method here contains only boilerplate code to bring up a visible dialog.
*/
@Override
public void run() {
final JDialog dialog = new JDialog();
JComponent content = createContent();
dialog.getContentPane().add(content, BorderLayout.CENTER);
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dialog.pack();
dialog.setLocationRelativeTo(null);
dialog.setVisible(true);
}
/**
* Creates the JTable and demonstrates the issue.
* @return the JTable to display.
*/
protected JComponent createContent() {
// (1) Create a TableModel that derives from AbstractTableModel
var model = new DefaultTableModel(new String[][] { { "One", "Orange" }, { "Two", "Green" } }, new String[] { "First", "Second" });
// (2) Create a JTable with that model
JTable table = new JTable(model);
// (3) Enable AutoCreateRowSorter (or set any RowSorter that derives from DefaultRowSorter)
table.setAutoCreateRowSorter(true);
// (4) Set at least one Comparator to any column
((DefaultRowSorter<?, ?>) table.getRowSorter()).setComparator(0, Comparator.naturalOrder());
// (5) Add a TableModelListener that resets the RowFilter on TableStructureChanged events
model.addTableModelListener(e -> {
if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
DefaultRowSorter<?, ?> rowSorter = (DefaultRowSorter<?, ?>) table.getRowSorter();
// The following line will create an java.lang.ArrayIndexOutOfBoundsException in DefaultRowSorter.getComparator
// The method getComparator already calls checkColumn to check the parameter value, but that check is done using the current value of the TableModel
// which is already updated to be three. But the Arrays of comparators wan't updated yet.
// There is no issue if the column count is reduced, but the issue occurs when the column count is increased.
rowSorter.setRowFilter(null);
}
});
// (6) At a later point in time -> increase the column count
SwingUtilities.invokeLater(() -> model.setColumnCount(3));
// NOTE: This is a very simple example. A read-world example may have a custom TableModel that adjusts its own structure due to some external events.
return table;
}
});
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Reset the RowSorter before increasing the columns of the TableModel.
For this workaround to be applicable the code that adjust the TableModel must also have access to the (or all) JTables that display the Model. If the TableModel is independent from any view and adjusts its own column count, then this workaround cannot be used.
Another workaround could be to "fix the issue" in the derived TableModel implementation and override javax.swing.table.AbstractTableModel.fireTableChanged(TableModelEvent) and notify all listeners in registration order.
FREQUENCY : always