java – When using TableCellRenderer to display a localdate in format dd.mm.yyyy my JTable search function does not work as expected

So, based on this example, I dove into the Java source code and extracted the RowFilter.dateFilter and modified it to support LocalDate directly. This means, you get all the features and functionality of RowFilter.dateFilterbut with LocalDate instead of Date.

If I was providing a searchable solution for a mixed series of values, I would design a dedicated “filter component” which provided the mechanisms for allowing a user to configure the filter them selves. In this case, that would mean allowing the user to, independently, configure filter parameters for the date values ​​(ie, included/exclude a given range, filter by month or year or straight up, only this day)

This would then build the RowSorter accordingly, based on the user preferences

The following example is overly simplistic and is intended only a demonstration of how you could customise a RowSorter to work with different data types.

To search by a date value, you must supply a valid date in the format of dd/MM/yyyy.

The example is making use of RowFilter.orFilterso it will either match the LocalDateFilter OR the RowFilter.regexFilter

Again, this is intended as a demonstration only.

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.RowFilter.ComparisonType;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableRowSorter;

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {
        private JTable table;

        private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");

        public TestPane() {
            setLayout(new BorderLayout());
            table = new JTable();
            DefaultTableModel model = new DefaultTableModel(
                    new Object[][]{
                        {"A", 1, LocalDate.parse("12/05/2000", formatter)},
                        {"B", 2, LocalDate.parse("12/06/2000", formatter)},
                        {"C", 3, LocalDate.parse("12/07/2000", formatter)},
                        {"D", 4, LocalDate.parse("12/08/2000", formatter)},
                        {"E", 5, LocalDate.parse("12/09/2000", formatter)},
                        {"F", 1, LocalDate.parse("12/10/2000", formatter)},
                        {"G", 2, LocalDate.parse("12/11/2000", formatter)},
                        {"H", 3, LocalDate.parse("12/12/2000", formatter)},
                        {"I", 4, LocalDate.parse("12/01/1990", formatter)},
                        {"J", 5, LocalDate.parse("12/02/1990", formatter)},
                        {"K", 1, LocalDate.parse("12/03/1990", formatter)},
                        {"L", 2, LocalDate.parse("12/04/1995", formatter)},
                        {"M", 3, LocalDate.parse("12/05/1995", formatter)},
                        {"N", 4, LocalDate.parse("12/06/1995", formatter)},
                        {"O", 5, LocalDate.parse("12/07/1995", formatter)},
                        {"P", 1, LocalDate.parse("12/08/1980", formatter)},
                        {"Q", 2, LocalDate.parse("12/09/1980", formatter)},
                        {"R", 3, LocalDate.parse("12/10/1980", formatter)},
                        {"S", 4, LocalDate.parse("12/11/1980", formatter)},
                        {"T", 5, LocalDate.parse("12/12/1980", formatter)},
                        {"U", 1, LocalDate.parse("12/01/1985", formatter)},
                        {"V", 2, LocalDate.parse("12/02/1985", formatter)},
                        {"W", 3, LocalDate.parse("12/03/1985", formatter)},
                        {"X", 4, LocalDate.parse("12/04/1985", formatter)},
                        {"Y", 5, LocalDate.parse("12/05/1985", formatter)},
                        {"Z", 1, LocalDate.parse("12/06/1985", formatter)},},
                    new Object[]{"Name", "Number", "Date"}) {
                @Override
                public Class<?> getColumnClass(int columnIndex) {
                    switch (columnIndex) {
                        case 0:
                            return String.class;
                        case 1:
                            return Integer.class;
                        case 2:
                            return LocalDate.class;
                    }
                    return Object.class;
                }

            };

            table.setModel(model);
            table.setAutoCreateRowSorter(true);
            table.setDefaultRenderer(LocalDate.class, new LocalDateTableCellRenderer(formatter));
            add(new JScrollPane(table));

            JTextField textField = new JTextField(10);
            add(textField, BorderLayout.SOUTH);
            textField.addActionListener(new ActionListener() {

                protected LocalDate toLocalDate(String text) {
                    try {
                        return LocalDate.parse(textField.getText(), formatter);
                    } catch (DateTimeParseException exp) {
                        return null;
                    }
                }

                @Override
                public void actionPerformed(ActionEvent e) {
                    String text = textField.getText();
                    if (text.isEmpty()) {
                        table.setRowSorter(null);
                        return;
                    }
                    List<RowFilter<Object, Object>> filters = new ArrayList<>(2);
                    // Include the date only if we can parse the text
                    LocalDate searchDate = toLocalDate(text);
                    if (searchDate != null) {
                        filters.add(new LocalDateFilter(ComparisonType.EQUAL, searchDate, 2));
                        // You can also support date rangers if you want
                        //filters.add(new LocalDateFilter(ComparisonType.BEFORE, searchDate, 2));
                        //filters.add(new LocalDateFilter(ComparisonType.AFTER, searchDate, 2));
                    }
                    // OR filter every thing else...
                    filters.add(RowFilter.regexFilter("(?i)" + text, new int[]{0, 1}));
                    TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>((DefaultTableModel) table.getModel());
                    sorter.setRowFilter(RowFilter.orFilter(filters));

                    table.setRowSorter(sorter);
                }
            });
        }
    }

    public class LocalDateTableCellRenderer extends DefaultTableCellRenderer {

        private DateTimeFormatter formatter;

        public LocalDateTableCellRenderer(DateTimeFormatter formatter) {
            this.formatter = formatter;
        }

        public DateTimeFormatter getFormatter() {
            return formatter;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            if (value instanceof LocalDate) {
                setText(getFormatter().format((LocalDate) value));
            }
            return this;
        }
    }

    public abstract class AbstractFilter<M, I> extends RowFilter<M, I> {
        private int[] columns;

        public AbstractFilter(int[] columns) {
            this.columns = columns;
        }

        @Override
        public boolean include(RowFilter.Entry<? extends M, ? extends I> value) {
            int count = value.getValueCount();
            if (columns.length > 0) {
                for (int i = columns.length - 1; i >= 0; i--) {
                    int index = columns[i];
                    if (index < count) {
                        if (include(value, index)) {
                            return true;
                        }
                    }
                }
            } else {
                while (--count >= 0) {
                    if (include(value, count)) {
                        return true;
                    }
                }
            }
            return false;
        }

        protected abstract boolean include(RowFilter.Entry<? extends M, ? extends I> value, int index);
    }

    public class LocalDateFilter<M, I> extends AbstractFilter<M, I> {
        private LocalDate date;
        private RowFilter.ComparisonType type;

        public LocalDateFilter(RowFilter.ComparisonType type, LocalDate date, int column) {
            this(type, date, new int[]{column});
        }

        public LocalDateFilter(RowFilter.ComparisonType type, LocalDate date, int[] columns) {
            super(columns);
            if (type == null) {
                throw new IllegalArgumentException("type must be non-null");
            }
            this.type = type;
            this.date = date;
        }

        @Override
        protected boolean include(RowFilter.Entry<? extends M, ? extends I> value, int index) {
            Object v = value.getValue(index);

            if (v instanceof LocalDate) {
                LocalDate vDate = (LocalDate) v;
                switch (type) {
                    case BEFORE:
                        return (vDate.isBefore(date));
                    case AFTER:
                        return (vDate.isAfter(date));
                    case EQUAL:
                        return (vDate.equals(date));
                    case NOT_EQUAL:
                        return !(vDate.equals(date));
                    default:
                        break;
                }
            }
            return false;
        }
    }

}

You “could” modify the LocalDateFilter to convert the date values ​​to String via a supplied DateTimeFormatterbut I’d be concerned that you’re mixing presentation and data concepts inappropriately, as they should be agnostic to each other – but that’s me.


There’s a little bit of discussion over if a renderer should be used to “change the text” of data item or not. Honestly, I don’t think it’s black and white. There are times when you, hostly, don’t want that kind of feature, but for me, a view is a “visual representation of data”, you shouldn’t “need” to massage the data for it to be presented via the view, that’s not the job of the model, that’s the job of the view.

For example, I could have the same data represented in a JListwith a short or long date format, based on the needs of the application and what the app is trying to present to the user – even more, it could be a user supplied configuration which needs to be applied – these are domains of the renderers /views, the data should need to be changed (IMHO).

So, copy’n’paste. When I copied a row (Command+C) it copied A 1 2000-05-12, so, nothing to be done here. If you need to change “how” it’s copied (ie, you’d like to present it as ddd MMM yyyy Instead, you could do something like this for example – the important thing here is, you’ve got control)

As for accessibility, if you have a read through How to Support Assistive Technologies you will note that accessibility is provided via such functionality as toolTipText and JComponent#getAccessibleContextwhich are methods of the cell render, so, again, it’s not an issue that would drive me to “massage” my data.

Again, context is king. There may be times when a wrapper/proxy object is worth the effort, but I’d always be asking myself, “what am I losing?” and “what am I gaining?” by doing it and how many of these do I need to create so I present it in a JTable or JComboBox or JList or some other component – but, that’s me.

Leave a Comment