mirror of https://github.com/apache/jmeter.git
Bug 52962 - Allow sorting by columns for View Results in Table, Summary Report, Aggregate Report and Aggregate Graph
Based on a contribution by Logan Mauzaize (logan.mauzaize at gmail.com) and Maxime Chassagneux
This closes github pr #245
Bugzilla Id: 52962
git-svn-id: https://svn.apache.org/repos/asf/jmeter/trunk@1778767 13f79535-47bb-0310-9956-ffa450edef68
Former-commit-id: 343a9428b1
This commit is contained in:
parent
52654fada2
commit
ca31116432
|
|
@ -69,7 +69,7 @@ import org.apache.jmeter.gui.action.ActionRouter;
|
|||
import org.apache.jmeter.gui.action.SaveGraphics;
|
||||
import org.apache.jmeter.gui.util.FileDialoger;
|
||||
import org.apache.jmeter.gui.util.FilePanel;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRenderer;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRendererWrapper;
|
||||
import org.apache.jmeter.gui.util.VerticalPanel;
|
||||
import org.apache.jmeter.samplers.Clearable;
|
||||
import org.apache.jmeter.samplers.SampleResult;
|
||||
|
|
@ -80,6 +80,7 @@ import org.apache.jorphan.gui.GuiUtils;
|
|||
import org.apache.jorphan.gui.JLabeledTextField;
|
||||
import org.apache.jorphan.gui.NumberRenderer;
|
||||
import org.apache.jorphan.gui.ObjectTableModel;
|
||||
import org.apache.jorphan.gui.ObjectTableSorter;
|
||||
import org.apache.jorphan.gui.RateRenderer;
|
||||
import org.apache.jorphan.gui.RendererUtils;
|
||||
import org.apache.jorphan.logging.LoggingManager;
|
||||
|
|
@ -94,7 +95,7 @@ import org.apache.log.Logger;
|
|||
*
|
||||
*/
|
||||
public class StatGraphVisualizer extends AbstractVisualizer implements Clearable, ActionListener {
|
||||
private static final long serialVersionUID = 240L;
|
||||
private static final long serialVersionUID = 241L;
|
||||
|
||||
private static final String PCT1_LABEL = JMeterUtils.getPropDefault("aggregate_rpt_pct1", "90");
|
||||
private static final String PCT2_LABEL = JMeterUtils.getPropDefault("aggregate_rpt_pct2", "95");
|
||||
|
|
@ -319,8 +320,8 @@ public class StatGraphVisualizer extends AbstractVisualizer implements Clearable
|
|||
new Functor("getSentKBPerSecond") }, //$NON-NLS-1$
|
||||
new Functor[] { null, null, null, null, null, null, null, null, null, null, null, null, null },
|
||||
new Class[] { String.class, Long.class, Long.class, Long.class, Long.class,
|
||||
Long.class, Long.class, Long.class, Long.class, String.class,
|
||||
String.class, String.class, String.class});
|
||||
Long.class, Long.class, Long.class, Long.class, Double.class,
|
||||
Double.class, Double.class, Double.class});
|
||||
}
|
||||
|
||||
// Column formats
|
||||
|
|
@ -467,9 +468,10 @@ public class StatGraphVisualizer extends AbstractVisualizer implements Clearable
|
|||
mainPanel.add(makeTitlePanel());
|
||||
|
||||
myJTable = new JTable(model);
|
||||
myJTable.setRowSorter(new ObjectTableSorter(model).fixLastRow());
|
||||
JMeterUtils.applyHiDPI(myJTable);
|
||||
// Fix centering of titles
|
||||
myJTable.getTableHeader().setDefaultRenderer(new HeaderAsPropertyRenderer(getColumnsMsgParameters()));
|
||||
HeaderAsPropertyRendererWrapper.setupDefaultRenderer(myJTable, getColumnsMsgParameters());
|
||||
myJTable.setPreferredScrollableViewportSize(new Dimension(500, 70));
|
||||
RendererUtils.applyRenderers(myJTable, getRenderers());
|
||||
myScrollPane = new JScrollPane(myJTable);
|
||||
|
|
@ -503,6 +505,7 @@ public class StatGraphVisualizer extends AbstractVisualizer implements Clearable
|
|||
});
|
||||
|
||||
spane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
spane.setOneTouchExpandable(true);
|
||||
spane.setLeftComponent(myScrollPane);
|
||||
spane.setRightComponent(tabbedGraph);
|
||||
spane.setResizeWeight(.2);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import javax.swing.border.Border;
|
|||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
import org.apache.jmeter.gui.util.FileDialoger;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRenderer;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRendererWrapper;
|
||||
import org.apache.jmeter.samplers.Clearable;
|
||||
import org.apache.jmeter.samplers.SampleResult;
|
||||
import org.apache.jmeter.save.CSVSaveService;
|
||||
|
|
@ -48,6 +48,7 @@ import org.apache.jmeter.testelement.TestElement;
|
|||
import org.apache.jmeter.util.JMeterUtils;
|
||||
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;
|
||||
import org.apache.jorphan.gui.ObjectTableModel;
|
||||
import org.apache.jorphan.gui.ObjectTableSorter;
|
||||
import org.apache.jorphan.gui.RendererUtils;
|
||||
|
||||
/**
|
||||
|
|
@ -59,7 +60,7 @@ import org.apache.jorphan.gui.RendererUtils;
|
|||
*/
|
||||
public class StatVisualizer extends AbstractVisualizer implements Clearable, ActionListener {
|
||||
|
||||
private static final long serialVersionUID = 240L;
|
||||
private static final long serialVersionUID = 241L;
|
||||
|
||||
private static final String USE_GROUP_NAME = "useGroupName"; //$NON-NLS-1$
|
||||
|
||||
|
|
@ -172,8 +173,9 @@ public class StatVisualizer extends AbstractVisualizer implements Clearable, Act
|
|||
mainPanel.add(makeTitlePanel());
|
||||
|
||||
myJTable = new JTable(model);
|
||||
myJTable.setRowSorter(new ObjectTableSorter(model).fixLastRow());
|
||||
JMeterUtils.applyHiDPI(myJTable);
|
||||
myJTable.getTableHeader().setDefaultRenderer(new HeaderAsPropertyRenderer(StatGraphVisualizer.getColumnsMsgParameters()));
|
||||
HeaderAsPropertyRendererWrapper.setupDefaultRenderer(myJTable, StatGraphVisualizer.getColumnsMsgParameters());
|
||||
myJTable.setPreferredScrollableViewportSize(new Dimension(500, 70));
|
||||
RendererUtils.applyRenderers(myJTable, StatGraphVisualizer.getRenderers());
|
||||
myScrollPane = new JScrollPane(myJTable);
|
||||
|
|
@ -219,4 +221,3 @@ public class StatVisualizer extends AbstractVisualizer implements Clearable, Act
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import javax.swing.border.EmptyBorder;
|
|||
import javax.swing.table.TableCellRenderer;
|
||||
|
||||
import org.apache.jmeter.gui.util.FileDialoger;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRenderer;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRendererWrapper;
|
||||
import org.apache.jmeter.samplers.Clearable;
|
||||
import org.apache.jmeter.samplers.SampleResult;
|
||||
import org.apache.jmeter.save.CSVSaveService;
|
||||
|
|
@ -53,6 +53,7 @@ import org.apache.jmeter.util.JMeterUtils;
|
|||
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;
|
||||
import org.apache.jorphan.gui.NumberRenderer;
|
||||
import org.apache.jorphan.gui.ObjectTableModel;
|
||||
import org.apache.jorphan.gui.ObjectTableSorter;
|
||||
import org.apache.jorphan.gui.RateRenderer;
|
||||
import org.apache.jorphan.gui.RendererUtils;
|
||||
import org.apache.jorphan.reflect.Functor;
|
||||
|
|
@ -63,7 +64,7 @@ import org.apache.jorphan.reflect.Functor;
|
|||
*/
|
||||
public class SummaryReport extends AbstractVisualizer implements Clearable, ActionListener {
|
||||
|
||||
private static final long serialVersionUID = 240L;
|
||||
private static final long serialVersionUID = 241L;
|
||||
|
||||
private static final String USE_GROUP_NAME = "useGroupName"; //$NON-NLS-1$
|
||||
|
||||
|
|
@ -158,8 +159,8 @@ public class SummaryReport extends AbstractVisualizer implements Clearable, Acti
|
|||
new Functor("getAvgPageBytes"), //$NON-NLS-1$
|
||||
},
|
||||
new Functor[] { null, null, null, null, null, null, null, null , null, null, null },
|
||||
new Class[] { String.class, Long.class, Long.class, Long.class, Long.class,
|
||||
String.class, String.class, String.class, String.class, String.class, String.class });
|
||||
new Class[] { String.class, Integer.class, Long.class, Long.class, Long.class,
|
||||
Double.class, Double.class, Double.class, Double.class, Double.class, Double.class });
|
||||
clearData();
|
||||
init();
|
||||
}
|
||||
|
|
@ -239,8 +240,9 @@ public class SummaryReport extends AbstractVisualizer implements Clearable, Acti
|
|||
mainPanel.add(makeTitlePanel());
|
||||
|
||||
myJTable = new JTable(model);
|
||||
myJTable.setRowSorter(new ObjectTableSorter(model).fixLastRow());
|
||||
JMeterUtils.applyHiDPI(myJTable);
|
||||
myJTable.getTableHeader().setDefaultRenderer(new HeaderAsPropertyRenderer());
|
||||
HeaderAsPropertyRendererWrapper.setupDefaultRenderer(myJTable);
|
||||
myJTable.setPreferredScrollableViewportSize(new Dimension(500, 70));
|
||||
RendererUtils.applyRenderers(myJTable, RENDERERS);
|
||||
myScrollPane = new JScrollPane(myJTable);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import java.awt.Color;
|
|||
import java.awt.FlowLayout;
|
||||
import java.text.Format;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Comparator;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.ImageIcon;
|
||||
|
|
@ -37,7 +38,7 @@ import javax.swing.border.EmptyBorder;
|
|||
import javax.swing.table.TableCellRenderer;
|
||||
|
||||
import org.apache.jmeter.JMeter;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRenderer;
|
||||
import org.apache.jmeter.gui.util.HeaderAsPropertyRendererWrapper;
|
||||
import org.apache.jmeter.gui.util.HorizontalPanel;
|
||||
import org.apache.jmeter.samplers.Clearable;
|
||||
import org.apache.jmeter.samplers.SampleResult;
|
||||
|
|
@ -45,6 +46,7 @@ import org.apache.jmeter.util.Calculator;
|
|||
import org.apache.jmeter.util.JMeterUtils;
|
||||
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;
|
||||
import org.apache.jorphan.gui.ObjectTableModel;
|
||||
import org.apache.jorphan.gui.ObjectTableSorter;
|
||||
import org.apache.jorphan.gui.RendererUtils;
|
||||
import org.apache.jorphan.gui.RightAlignRenderer;
|
||||
import org.apache.jorphan.gui.layout.VerticalLayout;
|
||||
|
|
@ -58,7 +60,7 @@ import org.apache.jorphan.reflect.Functor;
|
|||
*/
|
||||
public class TableVisualizer extends AbstractVisualizer implements Clearable {
|
||||
|
||||
private static final long serialVersionUID = 240L;
|
||||
private static final long serialVersionUID = 241L;
|
||||
|
||||
private static final String ICON_SIZE = JMeterUtils.getPropDefault(JMeter.TREE_ICON_SIZE, JMeter.DEFAULT_TREE_ICON_SIZE);
|
||||
|
||||
|
|
@ -185,10 +187,10 @@ public class TableVisualizer extends AbstractVisualizer implements Clearable {
|
|||
calc.addSample(res);
|
||||
int count = calc.getCount();
|
||||
TableSample newS = new TableSample(
|
||||
count,
|
||||
res.getSampleCount(),
|
||||
res.getStartTime(),
|
||||
res.getThreadName(),
|
||||
count,
|
||||
res.getSampleCount(),
|
||||
res.getStartTime(),
|
||||
res.getThreadName(),
|
||||
res.getSampleLabel(),
|
||||
res.getTime(),
|
||||
res.isSuccessful(),
|
||||
|
|
@ -238,8 +240,22 @@ public class TableVisualizer extends AbstractVisualizer implements Clearable {
|
|||
|
||||
// Set up the table itself
|
||||
table = new JTable(model);
|
||||
table.setRowSorter(new ObjectTableSorter(model).setValueComparator(5,
|
||||
Comparator.nullsFirst(
|
||||
(ImageIcon o1, ImageIcon o2) -> {
|
||||
if (o1 == o2) {
|
||||
return 0;
|
||||
}
|
||||
if (o1 == imageSuccess) {
|
||||
return -1;
|
||||
}
|
||||
if (o1 == imageFailure) {
|
||||
return 1;
|
||||
}
|
||||
throw new IllegalArgumentException("Only success and failure images can be compared");
|
||||
})));
|
||||
JMeterUtils.applyHiDPI(table);
|
||||
table.getTableHeader().setDefaultRenderer(new HeaderAsPropertyRenderer());
|
||||
HeaderAsPropertyRendererWrapper.setupDefaultRenderer(table);
|
||||
RendererUtils.applyRenderers(table, RENDERERS);
|
||||
|
||||
tableScrollPanel = new JScrollPane(table);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,19 @@ public class HeaderAsPropertyRenderer extends DefaultTableCellRenderer {
|
|||
* @return the text
|
||||
*/
|
||||
protected String getText(Object value, int row, int column) {
|
||||
return getText(value, row, column, columnsMsgParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text for the value as the translation of the resource name.
|
||||
*
|
||||
* @param value value for which to get the translation
|
||||
* @param column index which column message parameters should be used
|
||||
* @param row not used
|
||||
* @param columnsMsgParameters
|
||||
* @return the text
|
||||
*/
|
||||
static String getText(Object value, int row, int column, Object[][] columnsMsgParameters) {
|
||||
if (value == null){
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.apache.jmeter.gui.util;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
|
||||
/**
|
||||
* Wraps {@link TableCellRenderer} to renders items in a JTable by using resource names
|
||||
* and control some formatting (centering, fonts and border)
|
||||
*/
|
||||
public class HeaderAsPropertyRendererWrapper implements TableCellRenderer, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 240L;
|
||||
private Object[][] columnsMsgParameters;
|
||||
|
||||
private TableCellRenderer delegate;
|
||||
|
||||
/**
|
||||
* @param columnsMsgParameters Optional parameters of i18n keys
|
||||
*/
|
||||
public HeaderAsPropertyRendererWrapper(TableCellRenderer renderer, Object[][] columnsMsgParameters) {
|
||||
this.delegate = renderer;
|
||||
this.columnsMsgParameters = columnsMsgParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if(delegate instanceof DefaultTableCellRenderer) {
|
||||
DefaultTableCellRenderer tr = (DefaultTableCellRenderer) delegate;
|
||||
if (table != null) {
|
||||
JTableHeader header = table.getTableHeader();
|
||||
if (header != null){
|
||||
tr.setForeground(header.getForeground());
|
||||
tr.setBackground(header.getBackground());
|
||||
tr.setFont(header.getFont());
|
||||
}
|
||||
}
|
||||
tr.setBorder(UIManager.getBorder("TableHeader.cellBorder"));
|
||||
tr.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
}
|
||||
return delegate.getTableCellRendererComponent(table,
|
||||
HeaderAsPropertyRenderer.getText(value, row, column, columnsMsgParameters),
|
||||
isSelected, hasFocus, row, column);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param table {@link JTable}
|
||||
*/
|
||||
public static void setupDefaultRenderer(JTable table) {
|
||||
setupDefaultRenderer(table, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param table {@link JTable}
|
||||
* @param columnsMsgParameters Double dimension array of column message parameters
|
||||
*/
|
||||
public static void setupDefaultRenderer(JTable table, Object[][] columnsMsgParameters) {
|
||||
TableCellRenderer defaultRenderer = table.getTableHeader().getDefaultRenderer();
|
||||
HeaderAsPropertyRendererWrapper newRenderer = new HeaderAsPropertyRendererWrapper(defaultRenderer, columnsMsgParameters);
|
||||
table.getTableHeader().setDefaultRenderer(newRenderer);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,7 +23,6 @@ import java.util.Arrays;
|
|||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.event.TableModelEvent;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
|
||||
import org.apache.jorphan.logging.LoggingManager;
|
||||
|
|
@ -134,9 +133,8 @@ public class ObjectTableModel extends DefaultTableModel {
|
|||
}
|
||||
|
||||
public void clearData() {
|
||||
int size = getRowCount();
|
||||
objects.clear();
|
||||
super.fireTableRowsDeleted(0, size);
|
||||
super.fireTableDataChanged();
|
||||
}
|
||||
|
||||
public void addRow(Object value) {
|
||||
|
|
@ -149,12 +147,12 @@ public class ObjectTableModel extends DefaultTableModel {
|
|||
}
|
||||
}
|
||||
objects.add(value);
|
||||
super.fireTableRowsInserted(objects.size() - 1, objects.size());
|
||||
super.fireTableRowsInserted(objects.size() - 1, objects.size() - 1);
|
||||
}
|
||||
|
||||
public void insertRow(Object value, int index) {
|
||||
objects.add(index, value);
|
||||
super.fireTableRowsInserted(index, index + 1);
|
||||
super.fireTableRowsInserted(index, index);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
|
|
@ -202,12 +200,11 @@ public class ObjectTableModel extends DefaultTableModel {
|
|||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void moveRow(int start, int end, int to) {
|
||||
List<Object> subList = new ArrayList<>(objects.subList(start, end));
|
||||
for (int x = end - 1; x >= start; x--) {
|
||||
objects.remove(x);
|
||||
}
|
||||
objects.addAll(to, subList);
|
||||
super.fireTableChanged(new TableModelEvent(this));
|
||||
List<Object> subList = objects.subList(start, end);
|
||||
List<Object> backup = new ArrayList<>(subList);
|
||||
subList.clear();
|
||||
objects.addAll(to, backup);
|
||||
super.fireTableDataChanged();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
|
|
@ -292,9 +289,19 @@ public class ObjectTableModel extends DefaultTableModel {
|
|||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Object (List of Object)
|
||||
*/
|
||||
public Object getObjectList() { // used by TableEditor
|
||||
return objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return List of Object
|
||||
*/
|
||||
public List<Object> getObjectListAsList() {
|
||||
return objects;
|
||||
}
|
||||
|
||||
public void setRows(Iterable<?> rows) { // used by TableEditor
|
||||
clearData();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,356 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.jorphan.gui;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.swing.RowSorter;
|
||||
import javax.swing.SortOrder;
|
||||
|
||||
/**
|
||||
* Implementation of a {@link RowSorter} for {@link ObjectTableModel}
|
||||
* @since 3.2
|
||||
*
|
||||
*/
|
||||
public class ObjectTableSorter extends RowSorter<ObjectTableModel> {
|
||||
|
||||
/**
|
||||
* View row with model mapping. All data relates to model.
|
||||
*/
|
||||
public class Row {
|
||||
private int index;
|
||||
|
||||
protected Row(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return getModel().getObjectListAsList().get(getIndex());
|
||||
}
|
||||
|
||||
public Object getValueAt(int column) {
|
||||
return getModel().getValueAt(getIndex(), column);
|
||||
}
|
||||
}
|
||||
|
||||
protected class PreserveLastRowComparator implements Comparator<Row> {
|
||||
@Override
|
||||
public int compare(Row o1, Row o2) {
|
||||
int lastIndex = model.getRowCount() - 1;
|
||||
if (o1.getIndex() >= lastIndex || o2.getIndex() >= lastIndex) {
|
||||
return o1.getIndex() - o2.getIndex();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectTableModel model;
|
||||
private SortKey sortkey;
|
||||
|
||||
private Comparator<Row> comparator = null;
|
||||
private ArrayList<Row> viewToModel = new ArrayList<>();
|
||||
private int[] modelToView = new int[0];
|
||||
|
||||
private Comparator<Row> primaryComparator = null;
|
||||
private Comparator<?>[] valueComparators;
|
||||
private Comparator<Row> fallbackComparator;
|
||||
|
||||
public ObjectTableSorter(ObjectTableModel model) {
|
||||
this.model = model;
|
||||
|
||||
this.valueComparators = new Comparator<?>[this.model.getColumnCount()];
|
||||
IntStream.range(0, this.valueComparators.length).forEach(i -> this.setValueComparator(i, null));
|
||||
|
||||
setFallbackComparator(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator used prior to sorted columns.
|
||||
*/
|
||||
public Comparator<Row> getPrimaryComparator() {
|
||||
return primaryComparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator used on sorted columns.
|
||||
*/
|
||||
public Comparator<?> getValueComparator(int column) {
|
||||
return valueComparators[column];
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator if all sorted columns matches. Defaults to model index comparison.
|
||||
*/
|
||||
public Comparator<Row> getFallbackComparator() {
|
||||
return fallbackComparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator used prior to sorted columns.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public ObjectTableSorter setPrimaryComparator(Comparator<Row> primaryComparator) {
|
||||
invalidate();
|
||||
this.primaryComparator = primaryComparator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link #getPrimaryComparator() primary comparator} to one that don't sort last row.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public ObjectTableSorter fixLastRow() {
|
||||
return setPrimaryComparator(new PreserveLastRowComparator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign comparator to given column, if <code>null</code> a {@link #getDefaultComparator(int) default one} is used instead.
|
||||
* @param column Model column index.
|
||||
* @param comparator Column value comparator.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public ObjectTableSorter setValueComparator(int column, Comparator<?> comparator) {
|
||||
invalidate();
|
||||
if (comparator == null) {
|
||||
comparator = getDefaultComparator(column);
|
||||
}
|
||||
valueComparators[column] = comparator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a default comparator based on model column class. {@link Collator#getInstance()} for {@link String},
|
||||
* {@link Comparator#naturalOrder() natural order} for {@link Comparable}, no sort support for others.
|
||||
* @param column Model column index.
|
||||
*/
|
||||
protected Comparator<?> getDefaultComparator(int column) {
|
||||
Class<?> columnClass = model.getColumnClass(column);
|
||||
if (columnClass == null) {
|
||||
return null;
|
||||
}
|
||||
if (columnClass == String.class) {
|
||||
return Comparator.nullsFirst(Collator.getInstance());
|
||||
}
|
||||
if (Comparable.class.isAssignableFrom(columnClass)) {
|
||||
return Comparator.nullsFirst(Comparator.naturalOrder());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a fallback comparator (defaults to model index comparison) if none {@link #getPrimaryComparator() primary}, neither {@link #getValueComparator(int) column value comparators} can make differences between two rows.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public ObjectTableSorter setFallbackComparator(Comparator<Row> comparator) {
|
||||
invalidate();
|
||||
if (comparator == null) {
|
||||
comparator = Comparator.comparingInt(Row::getIndex);
|
||||
}
|
||||
fallbackComparator = comparator;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectTableModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggleSortOrder(int column) {
|
||||
SortKey newSortKey;
|
||||
if (isSortable(column)) {
|
||||
SortOrder newOrder = sortkey == null || sortkey.getColumn() != column
|
||||
|| sortkey.getSortOrder() != SortOrder.ASCENDING ? SortOrder.ASCENDING : SortOrder.DESCENDING;
|
||||
newSortKey = new SortKey(column, newOrder);
|
||||
} else {
|
||||
newSortKey = null;
|
||||
}
|
||||
setSortKey(newSortKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int convertRowIndexToModel(int index) {
|
||||
if (!isSorted()) {
|
||||
return index;
|
||||
}
|
||||
validate();
|
||||
return viewToModel.get(index).getIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int convertRowIndexToView(int index) {
|
||||
if (!isSorted()) {
|
||||
return index;
|
||||
}
|
||||
validate();
|
||||
return modelToView[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSortKeys(List<? extends SortKey> keys) {
|
||||
switch (keys.size()) {
|
||||
case 0:
|
||||
setSortKey(null);
|
||||
break;
|
||||
case 1:
|
||||
setSortKey(keys.get(0));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Only one column can be sorted");
|
||||
}
|
||||
}
|
||||
|
||||
public void setSortKey(SortKey sortkey) {
|
||||
if (Objects.equals(this.sortkey, sortkey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
if (sortkey != null) {
|
||||
int column = sortkey.getColumn();
|
||||
Comparator<?> comparator = valueComparators[column];
|
||||
if (comparator == null) {
|
||||
throw new IllegalArgumentException(format("Can't sort column %s, it is mapped to type %s and this one have no natural order. So an explicit one must be specified", column, model.getColumnClass(column)));
|
||||
}
|
||||
}
|
||||
this.sortkey = sortkey;
|
||||
this.comparator = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends SortKey> getSortKeys() {
|
||||
return isSorted() ? Collections.singletonList(sortkey) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewRowCount() {
|
||||
return getModelRowCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModelRowCount() {
|
||||
return model.getRowCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modelStructureChanged() {
|
||||
setSortKey(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void allRowsChanged() {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rowsInserted(int firstRow, int endRow) {
|
||||
rowsChanged(firstRow, endRow, false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rowsDeleted(int firstRow, int endRow) {
|
||||
rowsChanged(firstRow, endRow, true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rowsUpdated(int firstRow, int endRow) {
|
||||
rowsChanged(firstRow, endRow, true, true);
|
||||
}
|
||||
|
||||
protected void rowsChanged(int firstRow, int endRow, boolean deleted, boolean inserted) {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rowsUpdated(int firstRow, int endRow, int column) {
|
||||
if (isSorted(column)) {
|
||||
rowsUpdated(firstRow, endRow);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isSortable(int column) {
|
||||
return getValueComparator(column) != null;
|
||||
}
|
||||
|
||||
protected boolean isSorted(int column) {
|
||||
return isSorted() && sortkey.getColumn() == column && sortkey.getSortOrder() != SortOrder.UNSORTED;
|
||||
}
|
||||
|
||||
protected boolean isSorted() {
|
||||
return sortkey != null;
|
||||
}
|
||||
|
||||
protected void invalidate() {
|
||||
viewToModel.clear();
|
||||
modelToView = new int[0];
|
||||
}
|
||||
|
||||
protected void validate() {
|
||||
if (isSorted() && viewToModel.isEmpty()) {
|
||||
sort();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
protected Comparator<Row> getComparatorFromSortKey(SortKey sortkey) {
|
||||
Comparator comparator = getValueComparator(sortkey.getColumn());
|
||||
if (sortkey.getSortOrder() == SortOrder.DESCENDING) {
|
||||
comparator = comparator.reversed();
|
||||
}
|
||||
Function<Row,Object> getValueAt = (Row row) -> row.getValueAt(sortkey.getColumn());
|
||||
return Comparator.comparing(getValueAt, comparator);
|
||||
}
|
||||
|
||||
protected void sort() {
|
||||
if (comparator == null) {
|
||||
comparator = Stream.concat(
|
||||
Stream.concat(
|
||||
getPrimaryComparator() != null ? Stream.of(getPrimaryComparator()) : Stream.<Comparator<Row>>empty(),
|
||||
getSortKeys().stream().filter(sk -> sk != null && sk.getSortOrder() != SortOrder.UNSORTED).map(this::getComparatorFromSortKey)
|
||||
),
|
||||
Stream.of(getFallbackComparator())
|
||||
).reduce(comparator, (result, current) -> result != null ? result.thenComparing(current) : current);
|
||||
}
|
||||
|
||||
viewToModel.clear();
|
||||
viewToModel.ensureCapacity(model.getRowCount());
|
||||
IntStream.range(0, model.getRowCount()).mapToObj(i -> new Row(i)).forEach(viewToModel::add);
|
||||
Collections.sort(viewToModel, comparator);
|
||||
|
||||
updateModelToView();
|
||||
}
|
||||
|
||||
protected void updateModelToView() {
|
||||
modelToView = new int[viewToModel.size()];
|
||||
IntStream.range(0, viewToModel.size()).forEach(viewIndex -> modelToView[viewToModel.get(viewIndex).getIndex()] = viewIndex);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.jorphan.gui;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.stream.IntStream.range;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import javax.swing.event.TableModelEvent;
|
||||
|
||||
import org.apache.jorphan.reflect.Functor;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ObjectTableModelTest {
|
||||
|
||||
public static class Dummy {
|
||||
String a;
|
||||
String b;
|
||||
String c;
|
||||
|
||||
Dummy(String a, String b, String c) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.c = c;
|
||||
}
|
||||
|
||||
public String getA() {
|
||||
return a;
|
||||
}
|
||||
|
||||
public String getB() {
|
||||
return b;
|
||||
}
|
||||
|
||||
public String getC() {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectTableModel model;
|
||||
TableModelEventBacker events;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
String[] headers = { "a", "b", "c" };
|
||||
Functor[] readFunctors = Arrays.stream(headers).map(name -> "get" + name.toUpperCase()).map(Functor::new).toArray(n -> new Functor[n]);
|
||||
Functor[] writeFunctors = new Functor[headers.length];
|
||||
Class<?>[] editorClasses = new Class<?>[headers.length];
|
||||
Arrays.fill(editorClasses, String.class);
|
||||
model = new ObjectTableModel(headers, readFunctors, writeFunctors, editorClasses);
|
||||
events = new TableModelEventBacker();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkAddRow() {
|
||||
model.addTableModelListener(events);
|
||||
|
||||
assertModel();
|
||||
|
||||
model.addRow(new Dummy("1", "1", "1"));
|
||||
assertModel("1");
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.INSERT)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(0)
|
||||
.lastRow(0)
|
||||
);
|
||||
|
||||
model.addRow(new Dummy("2", "1", "1"));
|
||||
assertModel("1", "2");
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.INSERT)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(1)
|
||||
.lastRow(1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkClear() {
|
||||
// Arrange
|
||||
for (int i = 0; i < 5; i++) {
|
||||
model.addRow(new Dummy("" + i, "" + i%2, "" + i%3));
|
||||
}
|
||||
assertModelRanges(range(0,5));
|
||||
|
||||
// Act
|
||||
model.addTableModelListener(events);
|
||||
model.clearData();
|
||||
|
||||
// Assert
|
||||
assertModelRanges();
|
||||
|
||||
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.UPDATE)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(0)
|
||||
.lastRow(Integer.MAX_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkInsertRow() {
|
||||
assertModel();
|
||||
model.addRow(new Dummy("3", "1", "1"));
|
||||
assertModel("3");
|
||||
model.addTableModelListener(events);
|
||||
|
||||
model.insertRow(new Dummy("1", "1", "1"), 0);
|
||||
assertModel("1", "3");
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.INSERT)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(0)
|
||||
.lastRow(0)
|
||||
);
|
||||
|
||||
model.insertRow(new Dummy("2", "1", "1"), 1);
|
||||
assertModel("1", "2", "3");
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.INSERT)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(1)
|
||||
.lastRow(1)
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMoveRow_from_5_11_to_0() {
|
||||
// Arrange
|
||||
for (int i = 0; i < 20; i++) {
|
||||
model.addRow(new Dummy("" + i, "" + i%2, "" + i%3));
|
||||
}
|
||||
assertModelRanges(range(0, 20));
|
||||
|
||||
// Act
|
||||
model.addTableModelListener(events);
|
||||
model.moveRow(5, 11, 0);
|
||||
|
||||
// Assert
|
||||
assertModelRanges(range(5, 11), range(0, 5), range(11, 20));
|
||||
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.UPDATE)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(0)
|
||||
.lastRow(Integer.MAX_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMoveRow_from_0_6_to_0() {
|
||||
// Arrange
|
||||
for (int i = 0; i < 20; i++) {
|
||||
model.addRow(new Dummy("" + i, "" + i%2, "" + i%3));
|
||||
}
|
||||
assertModelRanges(range(0, 20));
|
||||
|
||||
// Act
|
||||
model.addTableModelListener(events);
|
||||
model.moveRow(0, 6, 0);
|
||||
|
||||
// Assert
|
||||
assertModelRanges(range(0, 20));
|
||||
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.UPDATE)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(0)
|
||||
.lastRow(Integer.MAX_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMoveRow_from_0_6_to_10() {
|
||||
// Arrange
|
||||
for (int i = 0; i < 20; i++) {
|
||||
model.addRow(new Dummy("" + i, "" + i%2, "" + i%3));
|
||||
}
|
||||
assertModelRanges(range(0, 20));
|
||||
|
||||
// Act
|
||||
model.addTableModelListener(events);
|
||||
model.moveRow(0, 6, 10);
|
||||
|
||||
// Assert
|
||||
assertModelRanges(range(6, 16), range(0, 6), range(16, 20));
|
||||
|
||||
events.assertEvents(
|
||||
events.assertEvent()
|
||||
.source(model)
|
||||
.type(TableModelEvent.UPDATE)
|
||||
.column(TableModelEvent.ALL_COLUMNS)
|
||||
.firstRow(0)
|
||||
.lastRow(Integer.MAX_VALUE)
|
||||
);
|
||||
}
|
||||
|
||||
private void assertModelRanges(IntStream... ranges) {
|
||||
IntStream ints = IntStream.empty();
|
||||
for (IntStream range : ranges) {
|
||||
ints = IntStream.concat(ints, range);
|
||||
}
|
||||
assertModel(ints.mapToObj(i -> "" + i).toArray(n -> new String[n]));
|
||||
}
|
||||
|
||||
private void assertModel(String... as) {
|
||||
assertEquals("model row count", as.length, model.getRowCount());
|
||||
|
||||
for (int row = 0; row < as.length; row++) {
|
||||
assertEquals(format("model[%d,0]", row), as[row], model.getValueAt(row, 0));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.jorphan.gui;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
import static org.hamcrest.CoreMatchers.sameInstance;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.AbstractMap.SimpleImmutableEntry;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import javax.swing.RowSorter.SortKey;
|
||||
import javax.swing.SortOrder;
|
||||
|
||||
import org.apache.jorphan.reflect.Functor;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ErrorCollector;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
public class ObjectTableSorterTest {
|
||||
ObjectTableModel model;
|
||||
ObjectTableSorter sorter;
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
@Rule
|
||||
public ErrorCollector errorCollector = new ErrorCollector();
|
||||
|
||||
@Before
|
||||
public void createModelAndSorter() {
|
||||
String[] headers = { "key", "value", "object" };
|
||||
Functor[] readFunctors = { new Functor("getKey"), new Functor("getValue"), new Functor("getValue") };
|
||||
Functor[] writeFunctors = { null, null, null };
|
||||
Class<?>[] editorClasses = { String.class, Integer.class, Object.class };
|
||||
model = new ObjectTableModel(headers, readFunctors, writeFunctors, editorClasses);
|
||||
sorter = new ObjectTableSorter(model);
|
||||
List<Entry<String,Integer>> data = asList(b2(), a3(), d4(), c1());
|
||||
data.forEach(model::addRow);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noSorting() {
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(b2(), a3(), d4(), c1());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortKeyAscending() {
|
||||
sorter.setSortKey(new SortKey(0, SortOrder.ASCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(a3(), b2(), c1(), d4());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortKeyDescending() {
|
||||
sorter.setSortKey(new SortKey(0, SortOrder.DESCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(d4(), c1(), b2(), a3());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortValueAscending() {
|
||||
sorter.setSortKey(new SortKey(1, SortOrder.ASCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(c1(), b2(), a3(), d4());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortValueDescending() {
|
||||
sorter.setSortKey(new SortKey(1, SortOrder.DESCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(d4(), a3(), b2(), c1());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void fixLastRowWithAscendingKey() {
|
||||
sorter.fixLastRow().setSortKey(new SortKey(0, SortOrder.ASCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(a3(), b2(), d4(), c1());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixLastRowWithDescendingKey() {
|
||||
sorter.fixLastRow().setSortKey(new SortKey(0, SortOrder.DESCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(d4(), b2(), a3(), c1());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixLastRowWithAscendingValue() {
|
||||
sorter.fixLastRow().setSortKey(new SortKey(1, SortOrder.ASCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(b2(), a3(), d4(), c1());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixLastRowWithDescendingValue() {
|
||||
sorter.fixLastRow().setSortKey(new SortKey(1, SortOrder.DESCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(d4(), a3(), b2(), c1());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customKeyOrder() {
|
||||
HashMap<String, Integer> customKeyOrder = asList("a", "c", "b", "d").stream().reduce(new HashMap<String,Integer>(), (map,key) -> { map.put(key, map.size()); return map; }, (a,b) -> a);
|
||||
Comparator<String> customKeyComparator = (a,b) -> customKeyOrder.get(a).compareTo(customKeyOrder.get(b));
|
||||
sorter.setValueComparator(0, customKeyComparator).setSortKey(new SortKey(0, SortOrder.ASCENDING));
|
||||
List<SimpleImmutableEntry<String, Integer>> expected = asList(a3(), c1(), b2(), d4());
|
||||
assertRowOrderAndIndexes(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDefaultComparatorForNullClass() {
|
||||
ObjectTableModel model = new ObjectTableModel(new String[] { "null" }, new Functor[] { null }, new Functor[] { null }, new Class<?>[] { null });
|
||||
ObjectTableSorter sorter = new ObjectTableSorter(model);
|
||||
|
||||
assertThat(sorter.getValueComparator(0), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDefaultComparatorForStringClass() {
|
||||
ObjectTableModel model = new ObjectTableModel(new String[] { "string" }, new Functor[] { null }, new Functor[] { null }, new Class<?>[] { String.class });
|
||||
ObjectTableSorter sorter = new ObjectTableSorter(model);
|
||||
|
||||
assertThat(sorter.getValueComparator(0), is(CoreMatchers.notNullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDefaultComparatorForIntegerClass() {
|
||||
ObjectTableModel model = new ObjectTableModel(new String[] { "integer" }, new Functor[] { null }, new Functor[] { null }, new Class<?>[] { Integer.class });
|
||||
ObjectTableSorter sorter = new ObjectTableSorter(model);
|
||||
|
||||
assertThat(sorter.getValueComparator(0), is(CoreMatchers.notNullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDefaultComparatorForObjectClass() {
|
||||
ObjectTableModel model = new ObjectTableModel(new String[] { "integer" }, new Functor[] { null }, new Functor[] { null }, new Class<?>[] { Object.class });
|
||||
ObjectTableSorter sorter = new ObjectTableSorter(model);
|
||||
|
||||
assertThat(sorter.getValueComparator(0), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_none() {
|
||||
assertSame(emptyList(), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_0() {
|
||||
sorter.toggleSortOrder(0);
|
||||
assertEquals(singletonList(new SortKey(0, SortOrder.ASCENDING)), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_0_1() {
|
||||
sorter.toggleSortOrder(0);
|
||||
sorter.toggleSortOrder(1);
|
||||
assertEquals(singletonList(new SortKey(1, SortOrder.ASCENDING)), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_0_0() {
|
||||
sorter.toggleSortOrder(0);
|
||||
sorter.toggleSortOrder(0);
|
||||
assertEquals(singletonList(new SortKey(0, SortOrder.DESCENDING)), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_0_0_0() {
|
||||
sorter.toggleSortOrder(0);
|
||||
sorter.toggleSortOrder(0);
|
||||
sorter.toggleSortOrder(0);
|
||||
assertEquals(singletonList(new SortKey(0, SortOrder.ASCENDING)), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_2() {
|
||||
sorter.toggleSortOrder(2);
|
||||
assertSame(emptyList(), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggleSortOrder_0_2() {
|
||||
sorter.toggleSortOrder(0);
|
||||
sorter.toggleSortOrder(2);
|
||||
assertSame(emptyList(), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSortKeys_none() {
|
||||
sorter.setSortKeys(new ArrayList<>());
|
||||
assertSame(Collections.emptyList(), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSortKeys_withSortedThenUnsorted() {
|
||||
sorter.setSortKeys(singletonList(new SortKey(0, SortOrder.ASCENDING)));
|
||||
sorter.setSortKeys(new ArrayList<>());
|
||||
assertSame(Collections.emptyList(), sorter.getSortKeys());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSortKeys_single() {
|
||||
List<SortKey> keys = singletonList(new SortKey(0, SortOrder.ASCENDING));
|
||||
sorter.setSortKeys(keys);
|
||||
assertThat(sorter.getSortKeys(), allOf( is(not(sameInstance(keys))), is(equalTo(keys)) ));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSortKeys_many() {
|
||||
expectedException.expect(IllegalArgumentException.class);
|
||||
|
||||
sorter.setSortKeys(asList(new SortKey(0, SortOrder.ASCENDING), new SortKey(1, SortOrder.ASCENDING)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSortKeys_invalidColumn() {
|
||||
expectedException.expect(IllegalArgumentException.class);
|
||||
|
||||
sorter.setSortKeys(Collections.singletonList(new SortKey(2, SortOrder.ASCENDING)));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected List<Entry<String,Integer>> actual() {
|
||||
return IntStream
|
||||
.range(0, sorter.getViewRowCount())
|
||||
.map(sorter::convertRowIndexToModel)
|
||||
.mapToObj(modelIndex -> (Entry<String,Integer>) sorter.getModel().getObjectListAsList().get(modelIndex))
|
||||
.collect(Collectors.toList())
|
||||
;
|
||||
}
|
||||
|
||||
protected SimpleImmutableEntry<String, Integer> d4() {
|
||||
return new AbstractMap.SimpleImmutableEntry<>("d", 4);
|
||||
}
|
||||
|
||||
protected SimpleImmutableEntry<String, Integer> c1() {
|
||||
return new AbstractMap.SimpleImmutableEntry<>("c", 1);
|
||||
}
|
||||
|
||||
protected SimpleImmutableEntry<String, Integer> b2() {
|
||||
return new AbstractMap.SimpleImmutableEntry<>("b", 2);
|
||||
}
|
||||
|
||||
protected SimpleImmutableEntry<String, Integer> a3() {
|
||||
return new AbstractMap.SimpleImmutableEntry<>("a", 3);
|
||||
}
|
||||
|
||||
protected void assertRowOrderAndIndexes(List<SimpleImmutableEntry<String, Integer>> expected) {
|
||||
assertEquals(expected, actual());
|
||||
assertRowIndexes();
|
||||
}
|
||||
|
||||
protected void assertRowIndexes() {
|
||||
IntStream
|
||||
.range(0, sorter.getViewRowCount())
|
||||
.forEach(viewIndex -> {
|
||||
int modelIndex = sorter.convertRowIndexToModel(viewIndex);
|
||||
errorCollector.checkThat(format("view(%d) model(%d)", viewIndex, modelIndex),
|
||||
sorter.convertRowIndexToView(modelIndex),
|
||||
CoreMatchers.equalTo(viewIndex));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.jorphan.gui;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.ObjIntConsumer;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
import javax.swing.event.TableModelEvent;
|
||||
import javax.swing.event.TableModelListener;
|
||||
|
||||
/**
|
||||
* Listener implementation that stores {@link TableModelEvent} and can make assertions against them.
|
||||
*/
|
||||
public class TableModelEventBacker implements TableModelListener {
|
||||
|
||||
/**
|
||||
* Makes assertions for a single {@link TableModelEvent}.
|
||||
*/
|
||||
public class EventAssertion {
|
||||
private List<ObjIntConsumer<TableModelEvent>> assertions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds an assertion first args is table model event, second one is event index.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion add(ObjIntConsumer<TableModelEvent> assertion) {
|
||||
assertions.add(assertion);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds assertion based on a {@link ToIntFunction to-int} transformation (examples: <code>TableModelEvent::getType</code>).
|
||||
* @param name Label for assertion reason
|
||||
* @param expected Expected value.
|
||||
* @param f {@link ToIntFunction to-int} transformation (examples: <code>TableModelEvent::getType</code>).
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion addInt(String name, int expected, ToIntFunction<TableModelEvent> f) {
|
||||
return add((e,i) -> assertEquals(format("%s[%d]", name, i), expected, f.applyAsInt(e)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link TableModelEvent#getSource()} assertion.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion source(Object expected) {
|
||||
return add((e,i) -> assertSame(format("source[%d]",i), expected, e.getSource()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link TableModelEvent#getType()} assertion.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion type(int expected) {
|
||||
return addInt("type", expected, TableModelEvent::getType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link TableModelEvent#getColumn()} assertion.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion column(int expected) {
|
||||
return addInt("column", expected, TableModelEvent::getColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link TableModelEvent#getFirstRow()} assertion.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion firstRow(int expected) {
|
||||
return addInt("firstRow", expected, TableModelEvent::getFirstRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link TableModelEvent#getLastRow()} assertion.
|
||||
* @return <code>this</code>
|
||||
*/
|
||||
public EventAssertion lastRow(int expected) {
|
||||
return addInt("lastRow", expected, TableModelEvent::getLastRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check assertion against provided value.
|
||||
* @param event Event to check
|
||||
* @param index Index.
|
||||
*/
|
||||
protected void assertEvent(TableModelEvent event, int index) {
|
||||
assertions.forEach(a -> a.accept(event, index));
|
||||
}
|
||||
}
|
||||
|
||||
private Deque<TableModelEvent> events = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* Stores event.
|
||||
*/
|
||||
@Override
|
||||
public void tableChanged(TableModelEvent e) {
|
||||
events.add(e);
|
||||
}
|
||||
|
||||
public Deque<TableModelEvent> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new event assertion.
|
||||
* @see #assertEvents(EventAssertion...)
|
||||
*/
|
||||
public EventAssertion assertEvent() {
|
||||
return new EventAssertion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each event assertion against each backed event in order. Event storage is cleared after it.
|
||||
*/
|
||||
public void assertEvents(EventAssertion... assertions) {
|
||||
try {
|
||||
assertEquals("event count", assertions.length, events.size());
|
||||
|
||||
int i = 0;
|
||||
for (TableModelEvent event : events) {
|
||||
assertions[i].assertEvent(event, i++);
|
||||
}
|
||||
} finally {
|
||||
events.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -124,6 +124,7 @@ Fill in some detail.
|
|||
<ul>
|
||||
<li><bug>60144</bug>View Results Tree : Add a more up to date Browser Renderer to replace old Render</li>
|
||||
<li><bug>60542</bug>View Results Tree : Allow Upper Panel to be collapsed. Contributed by Ubik Load Pack (support at ubikloadpack.com)</li>
|
||||
<li><bug>52962</bug>Allow sorting by columns for View Results in Table, Summary Report, Aggregate Report and Aggregate Graph. Based on a contribution by Logan Mauzaize (logan.mauzaize at gmail.com) and Maxime Chassagneux (maxime.chassagneux@gmail.com).</li>
|
||||
</ul>
|
||||
|
||||
<h3>Timers, Assertions, Config, Pre- & Post-Processors</h3>
|
||||
|
|
@ -147,7 +148,7 @@ Fill in some detail.
|
|||
<h3>General</h3>
|
||||
<ul>
|
||||
<li><bug>54525</bug>Search Feature : Enhance it with ability to replace</li>
|
||||
<li><bug>60530</bug>Add API to create JMeter threads while test is running. Based on a contribution by Logan Mauzaize and Maxime Chassagneux</li>
|
||||
<li><bug>60530</bug>Add API to create JMeter threads while test is running. Based on a contribution by Logan Mauzaize (logan.mauzaize at gmail.com) and Maxime Chassagneux (maxime.chassagneux@gmail.com).</li>
|
||||
</ul>
|
||||
|
||||
<ch_section>Non-functional changes</ch_section>
|
||||
|
|
@ -216,8 +217,8 @@ Fill in some detail.
|
|||
<li>(gavin at 16degrees.com.au)</li>
|
||||
<li>Thomas Schapitz (ts-nospam12 at online.de)</li>
|
||||
<li>Murdecai777 (https://github.com/Murdecai777)</li>
|
||||
<li>Logan Mauzaize (https://github.com/loganmzz)</li>
|
||||
<li>Maxime Chassagneux (https://github.com/max3163)</li>
|
||||
<li>Logan Mauzaize (logan.mauzaize at gmail.com)</li>
|
||||
<li>Maxime Chassagneux (maxime.chassagneux@gmail.com)</li>
|
||||
<li>忻隆 (298015902 at qq.com)</li>
|
||||
<li><a href="http://ubikloadpack.com">Ubik Load Pack</a></li>
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Reference in New Issue