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.dateFilter
, but with LocalDate
instead of Date
.
Caveat
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
Runnable example
The following example is overly simplistic and is intended only as 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.orFilter
, so 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 DateTimeFormatter
, but 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 JList
, with 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#getAccessibleContext
, which 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 the same data in a JTable
or JComboBox
or JList
or some other component - but, that's me.