You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by pm...@apache.org on 2017/01/14 13:18:06 UTC

svn commit: r1778767 - in /jmeter/trunk: src/components/org/apache/jmeter/visualizers/ src/core/org/apache/jmeter/gui/util/ src/jorphan/org/apache/jorphan/gui/ test/src/org/apache/jorphan/gui/ xdocs/

Author: pmouawad
Date: Sat Jan 14 13:18:06 2017
New Revision: 1778767

URL: http://svn.apache.org/viewvc?rev=1778767&view=rev
Log:
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

Added:
    jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java   (with props)
    jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java   (with props)
    jmeter/trunk/test/src/org/apache/jorphan/gui/
    jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java   (with props)
    jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java   (with props)
    jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java   (with props)
Modified:
    jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatGraphVisualizer.java
    jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatVisualizer.java
    jmeter/trunk/src/components/org/apache/jmeter/visualizers/SummaryReport.java
    jmeter/trunk/src/components/org/apache/jmeter/visualizers/TableVisualizer.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRenderer.java
    jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableModel.java
    jmeter/trunk/xdocs/changes.xml

Modified: jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatGraphVisualizer.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatGraphVisualizer.java?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatGraphVisualizer.java (original)
+++ jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatGraphVisualizer.java Sat Jan 14 13:18:06 2017
@@ -69,7 +69,7 @@ import org.apache.jmeter.gui.action.Acti
 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
                 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
         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
         });
 
         spane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+        spane.setOneTouchExpandable(true);
         spane.setLeftComponent(myScrollPane);
         spane.setRightComponent(tabbedGraph);
         spane.setResizeWeight(.2);

Modified: jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatVisualizer.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatVisualizer.java?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatVisualizer.java (original)
+++ jmeter/trunk/src/components/org/apache/jmeter/visualizers/StatVisualizer.java Sat Jan 14 13:18:06 2017
@@ -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.Tes
 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.RendererUt
  */
 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 Abst
         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 Abst
         }
     }
 }
-

Modified: jmeter/trunk/src/components/org/apache/jmeter/visualizers/SummaryReport.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/visualizers/SummaryReport.java?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/visualizers/SummaryReport.java (original)
+++ jmeter/trunk/src/components/org/apache/jmeter/visualizers/SummaryReport.java Sat Jan 14 13:18:06 2017
@@ -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.JMeterUtil
 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.Functo
  */
 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 Abstr
                     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 Abstr
         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);

Modified: jmeter/trunk/src/components/org/apache/jmeter/visualizers/TableVisualizer.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/visualizers/TableVisualizer.java?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/visualizers/TableVisualizer.java (original)
+++ jmeter/trunk/src/components/org/apache/jmeter/visualizers/TableVisualizer.java Sat Jan 14 13:18:06 2017
@@ -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.Functo
  */
 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 Abs
                     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 Abs
 
         // 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);

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRenderer.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRenderer.java?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRenderer.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRenderer.java Sat Jan 14 13:18:06 2017
@@ -78,6 +78,19 @@ public class HeaderAsPropertyRenderer ex
      * @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 "";
         }

Added: jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java?rev=1778767&view=auto
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java (added)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java Sat Jan 14 13:18:06 2017
@@ -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);
+    }
+
+}

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/util/HeaderAsPropertyRendererWrapper.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableModel.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableModel.java?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableModel.java (original)
+++ jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableModel.java Sat Jan 14 13:18:06 2017
@@ -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 De
     }
 
     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 De
             }
         }
         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 De
     /** {@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 De
         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();

Added: jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java?rev=1778767&view=auto
==============================================================================
--- jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java (added)
+++ jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java Sat Jan 14 13:18:06 2017
@@ -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);
+    }
+}

Propchange: jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/jorphan/org/apache/jorphan/gui/ObjectTableSorter.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java?rev=1778767&view=auto
==============================================================================
--- jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java (added)
+++ jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java Sat Jan 14 13:18:06 2017
@@ -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));
+        }
+    }
+
+}

Propchange: jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableModelTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java?rev=1778767&view=auto
==============================================================================
--- jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java (added)
+++ jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java Sat Jan 14 13:18:06 2017
@@ -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));
+            });
+
+    }
+}

Propchange: jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/test/src/org/apache/jorphan/gui/ObjectTableSorterTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java?rev=1778767&view=auto
==============================================================================
--- jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java (added)
+++ jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java Sat Jan 14 13:18:06 2017
@@ -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();
+        }
+    }
+
+
+}

Propchange: jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/test/src/org/apache/jorphan/gui/TableModelEventBacker.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: jmeter/trunk/xdocs/changes.xml
URL: http://svn.apache.org/viewvc/jmeter/trunk/xdocs/changes.xml?rev=1778767&r1=1778766&r2=1778767&view=diff
==============================================================================
--- jmeter/trunk/xdocs/changes.xml [utf-8] (original)
+++ jmeter/trunk/xdocs/changes.xml [utf-8] Sat Jan 14 13:18:06 2017
@@ -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- &amp; 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>\u5ffb\u9686 (298015902 at qq.com)</li>
 <li><a href="http://ubikloadpack.com">Ubik Load Pack</a></li>
 </ul>