0

I am trying to get my JTable to sort null values at the end of the table. This is similar to a thread found at SO (How can i sort java JTable with an empty Row and force the Empty row always be last?) and OTN (https://forums.oracle.com/thread/1351003) but the answers in those threads aren't very clear to me.

I did make a lot of progress using those answers, though, and I got as far as the SSCCE posted below. But I'm still trying to figure out how to get the null values (or, in the SSCCE I posted, the NullClassFillers) to always go to the bottom of the sort. I feel like if I could within the custom Comparator somehow know which direction was being sorted (ascending or descending), I could get this easily to work. But Comparator doesn't know the sort direction...

Anyone?

import java.awt.Component;
import java.util.Comparator;
import javax.swing.DefaultRowSorter;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;

public class Test {

    public static void main(String args[]) {
        JFrame frame = new JFrame();
        JTable table = new JTable();
        Object[][] data = new Object[8][3];
        data[0][0] = 6.5d; data[0][1] = "Name1";
        data[1][0] = new NullClassFiller(); data[1][1] = "Name2";
        data[2][0] = 2.6d; data[2][1] = "Name3";
        data[3][0] = 0d; data[3][1] = "Name4";
        data[4][0] = new NullClassFiller(); data[4][1] = "Name5";
        data[5][0] = -4d; data[5][1] = "Name6";
        data[6][0] = 0d; data[6][1] = "Name7";
        data[7][0] = -4.3d; data[7][1] = "Name8";
        table.setModel(new DefaultTableModel(data, new String[]{"One", "Two"}));
        table.setAutoCreateRowSorter(true);
        DefaultRowSorter<?, ?> sorter = (DefaultRowSorter<?, ?>) table.getRowSorter();
        sorter.setComparator(0, new CustomComparator());
        table.getColumnModel().getColumn(0).setCellRenderer(new CustomRenderer());
        JScrollPane pane = new JScrollPane(table);
        frame.add(pane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setSize(500, 500);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    static class CustomComparator implements Comparator<Object> {
        @Override
        public int compare(Object d1, Object d2) {
            if (d1 instanceof NullClassFiller) {
                return -1;
            } else if (d2 instanceof NullClassFiller) {
                return -1;
            } else {
                return ((Comparable<Object>) d1).compareTo(d2);
            }
        }
    }

    static class NullClassFiller {}

    static class CustomRenderer extends DefaultTableCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            DefaultTableCellRenderer renderer = (DefaultTableCellRenderer) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            if(value instanceof NullClassFiller)
                renderer.setText("");

            return renderer;
        }

    }
}
Community
  • 1
  • 1
ryvantage
  • 13,064
  • 15
  • 63
  • 112
  • possible duplicate of [Java Comparator always reading values as Strings](http://stackoverflow.com/questions/18680682/java-comparator-always-reading-values-as-strings) – mKorbel Sep 08 '13 at 05:53
  • 1
    hmm, don't quite understand your problem with the answer/s at OTN - it simply can't be done in a clean way, except by a complete re-write of the DefaultRowSorter and TableRowSorter. All other hacks will break sooner or later. – kleopatra Sep 08 '13 at 07:57
  • @kleopatra, the problem with the (working) solutions at OTN are: they are only worried about 1 singular empty row at the end, so most of them are hacks that involve something like `public static final Object EMPTY_ROW = "";`. That doesn't work for me because I have many empty rows. You say "it simply can't be done in a clean way, except by a complete re-write of the DefaultRowSorter and TableRowSorter." which is great, I'm willing to bite the bullet and completely rewrite, I just haven't the slightest clue how to do so. This is my attempt to do just that. – ryvantage Sep 08 '13 at 13:05

2 Answers2

0

I solved the problem thanks to @MadProgrammer and @kleopatra through a follow-up question (Java Comparator always reading values as Strings) I had.

The answer is to create a custom Comparator that can determine which way the table is being sorted, then return the right value accordingly (if the sort is ASCENDING then o1 needs to return 1, and -1 visa versa, and o2 is exactly the opposite).

In my opinion, which is not worth much, this is not TOO hackish and does solve my problem, so I'm satisfied, even if others are not.

Here is the complete code to my solution:

import java.awt.Component;
import java.util.Comparator;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SortOrder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

public class Test {

    public static void main(String args[]) {
        JFrame frame = new JFrame();
        Object[][] data = new Object[8][3];
        data[0][0] = 6.5d; data[0][1] = "Name1";
        data[1][0] = new NullClassFiller(); data[1][1] = "Name2";
        data[2][0] = 2.6d; data[2][1] = "Name3";
        data[3][0] = 0d; data[3][1] = "Name4";
        data[4][0] = new NullClassFiller(); data[4][1] = "Name5";
        data[5][0] = -4d; data[5][1] = "Name6";
        data[6][0] = 0d; data[6][1] = "Name7";
        data[7][0] = -4.3d; data[7][1] = "Name8";
        JTable table = new JTable();
        table.setModel(new DefaultTableModel(data, new String[]{"One", "Two"}) {
            @Override
            public Class<?> getColumnClass(int column) {
                return column == 0 ? Double.class : String.class;
            }
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        });

        TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel()) {
            @Override
            public Comparator<?> getComparator(final int column) {
                Comparator c = new Comparator() {
                    @Override
                    public int compare(Object o1, Object o2) {
                        boolean ascending = getSortKeys().get(column).getSortOrder() == SortOrder.ASCENDING;
                        System.out.println(o1.getClass() + " - " + o2.getClass());
                        if (o1 instanceof NullClassFiller) {
                            if(ascending)
                                return 1;
                            else
                                return -1;
                        } else if (o2 instanceof NullClassFiller) {
                            if(ascending)
                                return -1;
                            else
                                return 1;
                        } else {
                            return ((Comparable<Object>) o1).compareTo(o2);
                        }

                    }
                };
                return c;
            }
        };
        table.setRowSorter(sorter);
        table.getColumnModel().getColumn(0).setCellRenderer(new CustomRenderer());
        JScrollPane pane = new JScrollPane(table);
        frame.add(pane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setSize(500, 500);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    static class NullClassFiller {}

    static class CustomRenderer extends DefaultTableCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            DefaultTableCellRenderer renderer = (DefaultTableCellRenderer) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            if(value instanceof NullClassFiller)
                renderer.setText("");

            return renderer;
        }

    }
}

In my original post, I was creating a custom Comparator, but it had no access to getSortKeys(). So, I overwrote the getComparator() method in the TableRowSorter so that I could access getSortKeys(). It works.

ryvantage
  • 13,064
  • 15
  • 63
  • 112
  • 1
    sorry, that's still wrong, please read my answer to your other question carefully (here you only solve 1 out of 3 bullets). Also I I don't understand why you don't re-use Walter's [wrapping solution](https://forums.oracle.com/message/5691827#5691827) - that's by far the second best (short of re-writing the sorter) solution I've ever seen and indeed _does_ solve your use-case (without requiring a nullFiller). – kleopatra Sep 08 '13 at 13:48
  • plus your model implementation is very wrong: if the model returns a column type other than object, it **must** guarantee to only contain that type. Yours doesn't for the first ... – kleopatra Sep 08 '13 at 13:57
  • _by far the second best_ except that it has a small bug: the wrapper must return EMPTY_LAST_ROW (vs. NULL_VALUE) if the real model value is null – kleopatra Sep 08 '13 at 14:22
  • Walter's wrapper solution doesn't work because it only works for one row. His comparator does an equality check to see if the object equals the empty row (`boolean empty1 = o1 == EMPTY_ROW;`). My problem has multiple empty rows. Plus, I'm staring at his solution and trying to figure out what the heck it is doing. Copy/paste isn't my thing, I want to know/understand it. – ryvantage Sep 08 '13 at 18:39
  • Would you be happier if instead of a `NullClassFiller` I used `Double.NaN`? That way a double is always returned, and I could just check `value.isNan()`. I guess the only problem with that is I used `Double` for the SSCCE, but my real app uses `Integer`s... – ryvantage Sep 08 '13 at 18:41
  • I mean, really. I could create a custom wrapper class for the data being stored (`IntOrNull` or something) and it could either store an int value or a null and then check for that. Then `getColumnClass()` will return the custom wrapper class. – ryvantage Sep 08 '13 at 18:46
  • @kleopatra, how do you like this solution? http://www.vantagecp.com/Test.html I wrapped the `Double` in a custom class `CustomClassWrapper` and stored the `Double` value within, so I can test for null on the `Double` value inside the wrapper... – ryvantage Sep 08 '13 at 19:10
0

The call to getSortKeys().get() must use 0 (i.e. the main sort key) and not column:

boolean ascending = getSortKeys().get(0).getSortOrder() == SortOrder.ASCENDING;

If the user clicks, for example, over the column number 12 or 7, you want the first sort key (there will be 3 at most, if the user clicked before over other columns, to perform multi column sort). This sort key will have two attributes:

  1. Sort Order (ASCENDING, DESCENDING, UNORDERED)

    getSortKeys().get(0).getSortOrder()
    
  2. Column Index (12 or 7, for example)

    getSortKeys().get(0).getColumn()
    
Kevin Panko
  • 8,356
  • 19
  • 50
  • 61
jahey
  • 1