Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8068811

TableCellRenderer IndexOutOfBoundsException for sorted header row

XMLWordPrintable

      FULL PRODUCT VERSION :
      java version "1.7.0_71"
      Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
      Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)

      ADDITIONAL OS VERSION INFORMATION :
      Microsoft Windows [Version 6.1.7601]

      A DESCRIPTION OF THE PROBLEM :
      If sorting is enabled and DefaultTableCellRenderer is installed to render headers it can throw and IndexOutOfBoundsException on the header row when checking if the cell is editable.

      The contract for TableCellRenderer.getTableCellRendererComponent declares index of negative one as a legal index to represent the header row.

      The contract for JTable.isCellEditable does not specify illegal indexes.

      The contract for JTable.convertRowIndexToModel doesn't specify the range of illegal index values. It does declare that this method can be hostile toward illegal values.

      The contract for RowSorter.convertRowIndexToModel states that it throws if index is outside the range of the view. The header row is always in view.

      The contract for TableModel.isCellEditable doesn't declare any behavior on illegal values.

      The TableModel implementation should really decide if an index is valid or not since it should be possbile to create a JTable with an editor for the column header.

      The code should be patch as follows:
      Allow just negative one to be the only legal negative index in JTable.convertRowIndexToModel.

      ============================================
      public int convertRowIndexToModel(int viewRowIndex) {
              RowSorter sorter = getRowSorter();
              if (sorter != null && viewRowIndex != -1) {
                  return sorter.convertRowIndexToModel(viewRowIndex);
              }
              return viewRowIndex;
          }
      ===========================================

      Patch DefaultTableCellRenderer to trap index out of bounds exception and treat it as non editable if the header row is called.

      ============
      boolean editable;
              try {
                  editable = table.isCellEditable(row, column);
              } catch (IndexOutOfBoundsException e) {
                  if (row != -1 || column < 0 || column >= table.getColumnCount()) {
                      throw e;
                  }
              }
              if (!isSelected && editable) {
      ....
      ============================

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      This error occurs enough in production and I've reproduced it on test machine. The problem is that it is hard to reproduce this error. My hope is that presenting the API contracts will be enough to point out that spec from TableCellRenderer and implementation of everything else doesn't really deal with the header row.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      No exception is thrown.
      ACTUAL -
      An error message keeps flooding the console.

      ERROR MESSAGES/STACK TRACES THAT OCCUR :
      java.lang.IndexOutOfBoundsException: Invalid index
       at javax.swing.DefaultRowSorter.convertRowIndexToModel(Unknown Source)
       at javax.swing.JTable.convertRowIndexToModel(Unknown Source)
       at javax.swing.JTable.isCellEditable(Unknown Source)
       at javax.swing.table.DefaultTableCellRenderer.getTableCellRendererComponent(Unknown Source)
       at XXXXXX.java:81)
       at XXXXXX.java:507)
       at javax.swing.plaf.basic.BasicTableHeaderUI.getHeaderRenderer(Unknown Source)
       at javax.swing.plaf.basic.BasicTableHeaderUI.paintCell(Unknown Source)
       at javax.swing.plaf.basic.BasicTableHeaderUI.paint(Unknown Source)
       at javax.swing.plaf.ComponentUI.update(Unknown Source)
       at javax.swing.JComponent.paintComponent(Unknown Source)
       at javax.swing.JComponent.paint(Unknown Source)
       at javax.swing.JComponent.paintToOffscreen(Unknown Source)
       at javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(Unknown Source)
       at javax.swing.RepaintManager$PaintManager.paint(Unknown Source)
       at javax.swing.RepaintManager.paint(Unknown Source)
       at javax.swing.JComponent._paintImmediately(Unknown Source)
       at javax.swing.JComponent.paintImmediately(Unknown Source)
       at javax.swing.RepaintManager.paintDirtyRegions(Unknown Source)
       at javax.swing.RepaintManager.paintDirtyRegions(Unknown Source)
       at javax.swing.RepaintManager.prePaintDirtyRegions(Unknown Source)
       at javax.swing.RepaintManager.access$700(Unknown Source)
       at javax.swing.RepaintManager$ProcessingRunnable.run(Unknown Source)
       at java.awt.event.InvocationEvent.dispatch(Unknown Source)
       at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
       at java.awt.EventQueue.access$400(Unknown Source)
       at java.awt.EventQueue$2.run(Unknown Source)
       at java.awt.EventQueue$2.run(Unknown Source)
       at java.security.AccessController.doPrivileged(Native Method)
       at java.security.AccessControlContext$1.doIntersectionPrivilege(Unknown Source)
       at java.awt.EventQueue.dispatchEvent(Unknown Source)
       at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
       at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
       at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
       at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
       at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
       at java.awt.EventDispatchThread.run(Unknown Source)

      REPRODUCIBILITY :
      This bug can be reproduced rarely.

      ---------- BEGIN SOURCE ----------
      import java.awt.Component;
      import java.awt.IllegalComponentStateException;
      import javax.swing.*;
      import javax.swing.event.ListSelectionEvent;
      import javax.swing.event.ListSelectionListener;
      import javax.swing.table.DefaultTableCellRenderer;
      import javax.swing.table.DefaultTableModel;
      import javax.swing.table.JTableHeader;
      import javax.swing.table.TableModel;
       
      public class SorterHeaderIndex implements Runnable {
       
      public static void main(String[] args) throws Exception {
       SwingUtilities.invokeAndWait(new SorterHeaderIndex());
       }
       
      @Override
       public void run() {
       JFrame frame = new JFrame();
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       JDesktopPane desk = new JDesktopPane();
       JInternalFrame iframe = new JInternalFrame();
       iframe.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
       desk.add(iframe);
       JTable table = new JTable(createTable());
       table.setAutoCreateRowSorter(true);
       table.setCellSelectionEnabled(true);
       iframe.setContentPane(new JScrollPane(table));
       frame.setContentPane(desk);
       ColumnRenderer r = new ColumnRenderer(table);
       table.getTableHeader().setDefaultRenderer(r);
       table.getSelectionModel().addListSelectionListener(r);
       iframe.pack();
       iframe.setVisible(true);
       frame.setSize(800, 600);
       frame.validate();
       frame.setVisible(true);
       }
       
      private TableModel createTable() {
       DefaultTableModel dtm = new DefaultTableModel(10, 10);
       int cells = dtm.getRowCount() * dtm.getColumnCount();
       for (int i=0; i<cells; i++) {
       dtm.setValueAt(i, i / dtm.getRowCount(), i % dtm.getColumnCount());
       }
       return dtm;
       }
       
      private static class ColumnRenderer extends DefaultTableCellRenderer implements ListSelectionListener {
       
      private static final long serialVersionUID = 1L;
       private final JTable table;
       
      ColumnRenderer(final JTable table) {
       this.table = table;
       }
       
      @Override
       public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
       if (table != null && this.table != table) {
       throw new IllegalComponentStateException(table.toString());
       }
       
      if (column == table.convertColumnIndexToView(0)) {
       final ListSelectionModel lsm = table.getSelectionModel();
       final int min = lsm.getMinSelectionIndex();
       final int max = lsm.getMaxSelectionIndex();
       if (lsm.isSelectionEmpty()) {
       value = "#" + table.getRowCount();
       } else if (min == max) {
       value = "#" + (max + 1) + "/" + table.getRowCount();
       } else {
       value = "#" + (min + 1) + "-" + (max + 1) + "/" + table.getRowCount();
       }
       }
       return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
       }
       
      @Override
       public void valueChanged(final ListSelectionEvent e) {
       int column = table.convertColumnIndexToView(0);
       //code to repaint the header to show the cell number.
       final JTableHeader header = table.getTableHeader();
       header.paintImmediately(header.getHeaderRect(column));
       }
       }
      }
       
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Extend the DefaultTableCellRenderer, catch the index out of bounds exception and swap the state of 'isSelected' and perform the super call again. This will prevent the call to JTable.isCellEditable inside of the DefaultTableCellRenderer.

            mcherkas Mikhail Cherkasov (Inactive)
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: