Bläddra i källkod

Adding built-in sorting capability to _cat apis. (#20658)

* Adding built-in sorting capability to _cat apis.

Closes #16975

* addressing pr comments

* changing value types back to original implementation and fixing cosmetic issues

* Changing compareTo, hashCode of value types to a better implementation

* Changed value compareTos to use Double.compare instead of if statements + fixed some failed unit tests
Alexander Lin 9 år sedan
förälder
incheckning
10ddcc4616
37 ändrade filer med 714 tillägg och 32 borttagningar
  1. 16 2
      core/src/main/java/org/elasticsearch/common/Table.java
  2. 1 1
      core/src/main/java/org/elasticsearch/common/unit/ByteSizeUnit.java
  3. 10 7
      core/src/main/java/org/elasticsearch/common/unit/ByteSizeValue.java
  4. 10 10
      core/src/main/java/org/elasticsearch/common/unit/SizeValue.java
  5. 10 5
      core/src/main/java/org/elasticsearch/common/unit/TimeValue.java
  6. 109 3
      core/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java
  7. 13 0
      core/src/test/java/org/elasticsearch/common/TableTests.java
  8. 40 1
      core/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java
  9. 33 0
      core/src/test/java/org/elasticsearch/common/unit/SizeValueTests.java
  10. 33 0
      core/src/test/java/org/elasticsearch/common/unit/TimeValueTests.java
  11. 109 3
      core/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java
  12. 30 0
      docs/reference/cat.asciidoc
  13. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.aliases.json
  14. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.allocation.json
  15. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.count.json
  16. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.fielddata.json
  17. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.health.json
  18. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.help.json
  19. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json
  20. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.master.json
  21. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodeattrs.json
  22. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json
  23. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.pending_tasks.json
  24. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.plugins.json
  25. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.recovery.json
  26. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.repositories.json
  27. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.segments.json
  28. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.shards.json
  29. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.snapshots.json
  30. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.tasks.json
  31. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.templates.json
  32. 4 0
      rest-api-spec/src/main/resources/rest-api-spec/api/cat.thread_pool.json
  33. 43 0
      rest-api-spec/src/main/resources/rest-api-spec/test/cat.aliases/10_basic.yaml
  34. 54 0
      rest-api-spec/src/main/resources/rest-api-spec/test/cat.indices/10_basic.yaml
  35. 39 0
      rest-api-spec/src/main/resources/rest-api-spec/test/cat.repositories/10_basic.yaml
  36. 37 0
      rest-api-spec/src/main/resources/rest-api-spec/test/cat.shards/10_basic.yaml
  37. 47 0
      rest-api-spec/src/main/resources/rest-api-spec/test/cat.templates/10_basic.yaml

+ 16 - 2
core/src/main/java/org/elasticsearch/common/Table.java

@@ -30,8 +30,6 @@ import java.util.concurrent.TimeUnit;
 
 import static java.util.Collections.emptyMap;
 
-/**
- */
 public class Table {
 
     private List<Cell> headers = new ArrayList<>();
@@ -197,6 +195,22 @@ public class Table {
         return null;
     }
 
+    public Map<String, String> getAliasMap() {
+        Map<String, String> headerAliasMap = new HashMap<>();
+        for (int i = 0; i < headers.size(); i++) {
+            Cell headerCell = headers.get(i);
+            String headerName = headerCell.value.toString();
+            if (headerCell.attr.containsKey("alias")) {
+                String[] aliases = Strings.splitStringByCommaToArray(headerCell.attr.get("alias"));
+                for (String alias : aliases) {
+                    headerAliasMap.put(alias, headerName);
+                }
+            }
+            headerAliasMap.put(headerName, headerName);
+        }
+        return headerAliasMap;
+    }
+
     public static class Cell {
         public final Object value;
         public final Map<String, String> attr;

+ 1 - 1
core/src/main/java/org/elasticsearch/common/unit/ByteSizeUnit.java

@@ -269,4 +269,4 @@ public enum ByteSizeUnit implements Writeable {
     public static ByteSizeUnit readFrom(StreamInput in) throws IOException {
         return ByteSizeUnit.fromId(in.readVInt());
     }
-}
+}

+ 10 - 7
core/src/main/java/org/elasticsearch/common/unit/ByteSizeValue.java

@@ -29,7 +29,7 @@ import java.io.IOException;
 import java.util.Locale;
 import java.util.Objects;
 
-public class ByteSizeValue implements Writeable {
+public class ByteSizeValue implements Writeable, Comparable<ByteSizeValue> {
 
     private final long size;
     private final ByteSizeUnit unit;
@@ -191,15 +191,18 @@ public class ByteSizeValue implements Writeable {
             return false;
         }
 
-        ByteSizeValue sizeValue = (ByteSizeValue) o;
-
-        return getBytes() == sizeValue.getBytes();
+        return compareTo((ByteSizeValue) o) == 0;
     }
 
     @Override
     public int hashCode() {
-        int result = Long.hashCode(size);
-        result = 31 * result + (unit != null ? unit.hashCode() : 0);
-        return result;
+        return Double.hashCode(((double) size) * unit.toBytes(1));
+    }
+
+    @Override
+    public int compareTo(ByteSizeValue other) {
+        double thisValue = ((double) size) * unit.toBytes(1);
+        double otherValue = ((double) other.size) * other.unit.toBytes(1);
+        return Double.compare(thisValue, otherValue);
     }
 }

+ 10 - 10
core/src/main/java/org/elasticsearch/common/unit/SizeValue.java

@@ -27,7 +27,7 @@ import org.elasticsearch.common.io.stream.Writeable;
 
 import java.io.IOException;
 
-public class SizeValue implements Writeable {
+public class SizeValue implements Writeable, Comparable<SizeValue> {
 
     private final long size;
     private final SizeUnit sizeUnit;
@@ -201,18 +201,18 @@ public class SizeValue implements Writeable {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
 
-        SizeValue sizeValue = (SizeValue) o;
-
-        if (size != sizeValue.size) return false;
-        if (sizeUnit != sizeValue.sizeUnit) return false;
-
-        return true;
+        return compareTo((SizeValue) o) == 0;
     }
 
     @Override
     public int hashCode() {
-        int result = Long.hashCode(size);
-        result = 31 * result + (sizeUnit != null ? sizeUnit.hashCode() : 0);
-        return result;
+        return Double.hashCode(((double) size) * sizeUnit.toSingles(1));
+    }
+
+    @Override
+    public int compareTo(SizeValue other) {
+        double thisValue = ((double) size) * sizeUnit.toSingles(1);
+        double otherValue = ((double) other.size) * other.sizeUnit.toSingles(1);
+        return Double.compare(thisValue, otherValue);
     }
 }

+ 10 - 5
core/src/main/java/org/elasticsearch/common/unit/TimeValue.java

@@ -39,7 +39,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-public class TimeValue implements Writeable {
+public class TimeValue implements Writeable, Comparable<TimeValue> {
 
     /** How many nano-seconds in one milli-second */
     public static final long NSEC_PER_MSEC = TimeUnit.NANOSECONDS.convert(1, TimeUnit.MILLISECONDS);
@@ -381,17 +381,22 @@ public class TimeValue implements Writeable {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
 
-        TimeValue timeValue = (TimeValue) o;
-        return timeUnit.toNanos(duration) == timeValue.timeUnit.toNanos(timeValue.duration);
+        return this.compareTo(((TimeValue) o)) == 0;
     }
 
     @Override
     public int hashCode() {
-        long normalized = timeUnit.toNanos(duration);
-        return Long.hashCode(normalized);
+        return Double.hashCode(((double) duration) * timeUnit.toNanos(1));
     }
 
     public static long nsecToMSec(long ns) {
         return ns / NSEC_PER_MSEC;
     }
+
+    @Override
+    public int compareTo(TimeValue timeValue) {
+        double thisValue = ((double) duration) * timeUnit.toNanos(1);
+        double otherValue = ((double) timeValue.duration) * timeValue.timeUnit.toNanos(1);
+        return Double.compare(thisValue, otherValue);
+    }
 }

+ 109 - 3
core/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java

@@ -38,8 +38,12 @@ import org.elasticsearch.rest.RestStatus;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 public class RestTable {
@@ -59,13 +63,13 @@ public class RestTable {
         List<DisplayHeader> displayHeaders = buildDisplayHeaders(table, request);
 
         builder.startArray();
-        for (int row = 0; row < table.getRows().size(); row++) {
+        List<Integer> rowOrder = getRowOrder(table, request);
+        for (Integer row : rowOrder) {
             builder.startObject();
             for (DisplayHeader header : displayHeaders) {
                 builder.field(header.display, renderValue(request, table.getAsMap().get(header.name).get(row).value));
             }
             builder.endObject();
-
         }
         builder.endArray();
         return new BytesRestResponse(RestStatus.OK, builder);
@@ -92,7 +96,10 @@ public class RestTable {
             }
             out.append("\n");
         }
-        for (int row = 0; row < table.getRows().size(); row++) {
+
+        List<Integer> rowOrder = getRowOrder(table, request);
+
+        for (Integer row: rowOrder) {
             for (int col = 0; col < headers.size(); col++) {
                 DisplayHeader header = headers.get(col);
                 boolean isLastColumn = col == lastHeader;
@@ -107,6 +114,38 @@ public class RestTable {
         return new BytesRestResponse(RestStatus.OK, BytesRestResponse.TEXT_CONTENT_TYPE, bytesOut.bytes());
     }
 
+    static List<Integer> getRowOrder(Table table, RestRequest request) {
+        String[] columnOrdering = request.paramAsStringArray("s", null);
+
+        List<Integer> rowOrder = new ArrayList<>();
+        for (int i = 0; i < table.getRows().size(); i++) {
+            rowOrder.add(i);
+        }
+
+        if (columnOrdering != null) {
+            Map<String, String> headerAliasMap = table.getAliasMap();
+            List<ColumnOrderElement> ordering = new ArrayList<>();
+            for (int i = 0; i < columnOrdering.length; i++) {
+                String columnHeader = columnOrdering[i];
+                boolean reverse = false;
+                if (columnHeader.endsWith(":desc")) {
+                    columnHeader = columnHeader.substring(0, columnHeader.length() - ":desc".length());
+                    reverse = true;
+                } else if (columnHeader.endsWith(":asc")) {
+                    columnHeader = columnHeader.substring(0, columnHeader.length() - ":asc".length());
+                }
+                if (headerAliasMap.containsKey(columnHeader)) {
+                        ordering.add(new ColumnOrderElement(headerAliasMap.get(columnHeader), reverse));
+                } else {
+                    throw new UnsupportedOperationException(
+                        String.format(Locale.ROOT, "Unable to sort by unknown sort key `%s`", columnHeader));
+                }
+            }
+            Collections.sort(rowOrder, new TableIndexComparator(table, ordering));
+        }
+        return rowOrder;
+    }
+
     static List<DisplayHeader> buildDisplayHeaders(Table table, RestRequest request) {
         List<DisplayHeader> display = new ArrayList<>();
         if (request.hasParam("h")) {
@@ -368,4 +407,71 @@ public class RestTable {
             this.display = display;
         }
     }
+
+    static class TableIndexComparator implements Comparator<Integer> {
+        private final Table table;
+        private final int maxIndex;
+        private final List<ColumnOrderElement> ordering;
+
+        TableIndexComparator(Table table, List<ColumnOrderElement> ordering) {
+            this.table = table;
+            this.maxIndex = table.getRows().size();
+            this.ordering = ordering;
+        }
+
+        private int compareCell(Object o1, Object o2) {
+            if (o1 == null && o2 == null) {
+                return 0;
+            } else if (o1 == null) {
+                return -1;
+            } else if (o2 == null) {
+                return 1;
+            } else {
+                if (o1 instanceof Comparable && o1.getClass().equals(o2.getClass())) {
+                    return ((Comparable) o1).compareTo(o2);
+                } else {
+                    return o1.toString().compareTo(o2.toString());
+                }
+            }
+        }
+
+        @Override
+        public int compare(Integer rowIndex1, Integer rowIndex2) {
+            if (rowIndex1 < maxIndex && rowIndex1 >= 0 && rowIndex2 < maxIndex && rowIndex2 >= 0) {
+                Map<String, List<Table.Cell>> tableMap = table.getAsMap();
+                for (ColumnOrderElement orderingElement : ordering) {
+                    String column = orderingElement.getColumn();
+                    if (tableMap.containsKey(column)) {
+                        int comparison = compareCell(tableMap.get(column).get(rowIndex1).value,
+                            tableMap.get(column).get(rowIndex2).value);
+                        if (comparison != 0) {
+                            return orderingElement.isReversed() ? -1 * comparison : comparison;
+                        }
+                    }
+                }
+                return 0;
+            } else {
+                throw new AssertionError(String.format(Locale.ENGLISH, "Invalid comparison of indices (%s, %s): Table has %s rows.",
+                    rowIndex1, rowIndex2, table.getRows().size()));
+            }
+        }
+    }
+
+    static class ColumnOrderElement {
+        private final String column;
+        private final boolean reverse;
+
+        public ColumnOrderElement(String column, boolean reverse) {
+            this.column = column;
+            this.reverse = reverse;
+        }
+
+        public String getColumn() {
+            return column;
+        }
+
+        public boolean isReversed() {
+            return reverse;
+        }
+    }
 }

+ 13 - 0
core/src/test/java/org/elasticsearch/common/TableTests.java

@@ -200,6 +200,19 @@ public class TableTests extends ESTestCase {
 
     }
 
+    public void testAliasMap() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("asdf", "alias:a");
+        table.addCell("ghij", "alias:g,h");
+        table.endHeaders();
+        Map<String, String> aliasMap = table.getAliasMap();
+        assertEquals(5, aliasMap.size());
+        assertEquals("asdf", aliasMap.get("a"));
+        assertEquals("ghij", aliasMap.get("g"));
+        assertEquals("ghij", aliasMap.get("h"));
+    }
+
     private Table getTableWithHeaders() {
         Table table = new Table();
         table.startHeaders();

+ 40 - 1
core/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java

@@ -170,13 +170,52 @@ public class ByteSizeValueTests extends ESTestCase {
         }
     }
 
+    public void testCompareEquality() {
+        long firstRandom = randomPositiveLong();
+        ByteSizeUnit randomUnit = randomFrom(ByteSizeUnit.values());
+        ByteSizeValue firstByteValue = new ByteSizeValue(firstRandom, randomUnit);
+        ByteSizeValue secondByteValue = new ByteSizeValue(firstRandom, randomUnit);
+        assertEquals(0, firstByteValue.compareTo(secondByteValue));
+    }
+
+    public void testCompareValue() {
+        long firstRandom = randomPositiveLong();
+        long secondRandom = randomValueOtherThan(firstRandom, ESTestCase::randomPositiveLong);
+        ByteSizeUnit unit = randomFrom(ByteSizeUnit.values());
+        ByteSizeValue firstByteValue = new ByteSizeValue(firstRandom, unit);
+        ByteSizeValue secondByteValue = new ByteSizeValue(secondRandom, unit);
+        assertEquals(firstRandom > secondRandom, firstByteValue.compareTo(secondByteValue) > 0);
+        assertEquals(secondRandom > firstRandom, secondByteValue.compareTo(firstByteValue) > 0);
+    }
+
+    public void testCompareUnits() {
+        long number = randomPositiveLong();
+        ByteSizeUnit randomUnit = randomValueOtherThan(ByteSizeUnit.PB, ()->randomFrom(ByteSizeUnit.values()));
+        ByteSizeValue firstByteValue = new ByteSizeValue(number, randomUnit);
+        ByteSizeValue secondByteValue = new ByteSizeValue(number, ByteSizeUnit.PB);
+        assertTrue(firstByteValue.compareTo(secondByteValue) < 0);
+        assertTrue(secondByteValue.compareTo(firstByteValue) > 0);
+    }
+
+    public void testEdgeCompare() {
+        ByteSizeValue maxLongValuePB = new ByteSizeValue(Long.MAX_VALUE, ByteSizeUnit.PB);
+        ByteSizeValue maxLongValueB = new ByteSizeValue(Long.MAX_VALUE, ByteSizeUnit.BYTES);
+        assertTrue(maxLongValuePB.compareTo(maxLongValueB) > 0);
+    }
+
+    public void testConversionHashCode() {
+        ByteSizeValue firstValue = new ByteSizeValue(randomIntBetween(0, Integer.MAX_VALUE), ByteSizeUnit.GB);
+        ByteSizeValue secondValue = new ByteSizeValue(firstValue.getBytes(), ByteSizeUnit.BYTES);
+        assertEquals(firstValue.hashCode(), secondValue.hashCode());
+    }
+
     public void testSerialization() throws IOException {
         ByteSizeValue byteSizeValue = new ByteSizeValue(randomPositiveLong(), randomFrom(ByteSizeUnit.values()));
         try (BytesStreamOutput out = new BytesStreamOutput()) {
             byteSizeValue.writeTo(out);
             try (StreamInput in = out.bytes().streamInput()) {
                 ByteSizeValue deserializedByteSizeValue = new ByteSizeValue(in);
-                assertEquals(byteSizeValue, deserializedByteSizeValue);
+                assertEquals(byteSizeValue.getBytes(), deserializedByteSizeValue.getBytes());
             }
         }
     }

+ 33 - 0
core/src/test/java/org/elasticsearch/common/unit/SizeValueTests.java

@@ -67,4 +67,37 @@ public class SizeValueTests extends ESTestCase {
             assertThat(e.getMessage(), containsString("may not be negative"));
         }
     }
+
+    public void testCompareEquality() {
+        long randomValue = randomPositiveLong();
+        SizeUnit randomUnit = randomFrom(SizeUnit.values());
+        SizeValue firstValue = new SizeValue(randomValue, randomUnit);
+        SizeValue secondValue = new SizeValue(randomValue, randomUnit);
+        assertEquals(0, firstValue.compareTo(secondValue));
+    }
+
+    public void testCompareValue() {
+        long firstRandom = randomPositiveLong();
+        long secondRandom = randomValueOtherThan(firstRandom, ESTestCase::randomPositiveLong);
+        SizeUnit unit = randomFrom(SizeUnit.values());
+        SizeValue firstSizeValue = new SizeValue(firstRandom, unit);
+        SizeValue secondSizeValue = new SizeValue(secondRandom, unit);
+        assertEquals(firstRandom > secondRandom, firstSizeValue.compareTo(secondSizeValue) > 0);
+        assertEquals(secondRandom > firstRandom, secondSizeValue.compareTo(firstSizeValue) > 0);
+    }
+
+    public void testCompareUnits() {
+        long number = randomPositiveLong();
+        SizeUnit randomUnit = randomValueOtherThan(SizeUnit.PETA, ()->randomFrom(SizeUnit.values()));
+        SizeValue firstValue = new SizeValue(number, randomUnit);
+        SizeValue secondValue = new SizeValue(number, SizeUnit.PETA);
+        assertTrue(firstValue.compareTo(secondValue) < 0);
+        assertTrue(secondValue.compareTo(firstValue) > 0);
+    }
+
+    public void testConversionHashCode() {
+        SizeValue firstValue = new SizeValue(randomIntBetween(0, Integer.MAX_VALUE), SizeUnit.GIGA);
+        SizeValue secondValue = new SizeValue(firstValue.getSingles(), SizeUnit.SINGLE);
+        assertEquals(firstValue.hashCode(), secondValue.hashCode());
+    }
 }

+ 33 - 0
core/src/test/java/org/elasticsearch/common/unit/TimeValueTests.java

@@ -224,4 +224,37 @@ public class TimeValueTests extends ESTestCase {
         assertEquals("36h", new TimeValue(36, TimeUnit.HOURS).getStringRep());
         assertEquals("1000d", new TimeValue(1000, TimeUnit.DAYS).getStringRep());
     }
+
+    public void testCompareEquality() {
+        long randomLong = randomPositiveLong();
+        TimeUnit randomUnit = randomFrom(TimeUnit.values());
+        TimeValue firstValue = new TimeValue(randomLong, randomUnit);
+        TimeValue secondValue = new TimeValue(randomLong, randomUnit);
+        assertEquals(0, firstValue.compareTo(secondValue));
+    }
+
+    public void testCompareValue() {
+        long firstRandom = randomPositiveLong();
+        long secondRandom = randomValueOtherThan(firstRandom, ESTestCase::randomPositiveLong);
+        TimeUnit unit = randomFrom(TimeUnit.values());
+        TimeValue firstValue = new TimeValue(firstRandom, unit);
+        TimeValue secondValue = new TimeValue(secondRandom, unit);
+        assertEquals(firstRandom > secondRandom, firstValue.compareTo(secondValue) > 0);
+        assertEquals(secondRandom > firstRandom, secondValue.compareTo(firstValue) > 0);
+    }
+
+    public void testCompareUnits() {
+        long number = randomPositiveLong();
+        TimeUnit randomUnit = randomValueOtherThan(TimeUnit.DAYS, ()->randomFrom(TimeUnit.values()));
+        TimeValue firstValue = new TimeValue(number, randomUnit);
+        TimeValue secondValue = new TimeValue(number, TimeUnit.DAYS);
+        assertTrue(firstValue.compareTo(secondValue) < 0);
+        assertTrue(secondValue.compareTo(firstValue) > 0);
+    }
+
+    public void testConversionHashCode() {
+        TimeValue firstValue = new TimeValue(randomIntBetween(0, Integer.MAX_VALUE), TimeUnit.MINUTES);
+        TimeValue secondValue = new TimeValue(firstValue.getSeconds(), TimeUnit.SECONDS);
+        assertEquals(firstValue.hashCode(), secondValue.hashCode());
+    }
 }

+ 109 - 3
core/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java

@@ -23,12 +23,12 @@ import org.elasticsearch.common.Table;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.rest.AbstractRestChannel;
 import org.elasticsearch.rest.RestResponse;
-import org.elasticsearch.rest.action.cat.RestTable;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.rest.FakeRestRequest;
 import org.junit.Before;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -62,11 +62,13 @@ public class RestTableTests extends ESTestCase {
             "  invalidAliasesBulk: \"foo\"\n" +
             "  timestamp: \"foo\"\n" +
             "  epoch: \"foo\"\n";
-    private Table table = new Table();
-    private FakeRestRequest restRequest = new FakeRestRequest();
+    private Table table;
+    private FakeRestRequest restRequest;
 
     @Before
     public void setup() {
+        restRequest = new FakeRestRequest();
+        table = new Table();
         table.startHeaders();
         table.addCell("bulk.foo", "alias:f;desc:foo");
         table.addCell("bulk.bar", "alias:b;desc:bar");
@@ -146,6 +148,110 @@ public class RestTableTests extends ESTestCase {
         assertThat(headerNames, not(hasItem("epoch")));
     }
 
+    public void testCompareRow() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("compare");
+        table.endHeaders();
+
+        for (Integer i : Arrays.asList(1,2,1)) {
+            table.startRow();
+            table.addCell(i);
+            table.endRow();
+        }
+
+        RestTable.TableIndexComparator comparator = new RestTable.TableIndexComparator(table,
+            Collections.singletonList(new RestTable.ColumnOrderElement("compare", false)));
+        assertTrue(comparator.compare(0,1) < 0);
+        assertTrue(comparator.compare(0,2) == 0);
+        assertTrue(comparator.compare(1,2) > 0);
+
+        RestTable.TableIndexComparator reverseComparator = new RestTable.TableIndexComparator(table,
+            Collections.singletonList(new RestTable.ColumnOrderElement("compare", true)));
+
+        assertTrue(reverseComparator.compare(0,1) > 0);
+        assertTrue(reverseComparator.compare(0,2) == 0);
+        assertTrue(reverseComparator.compare(1,2) < 0);
+    }
+
+    public void testRowOutOfBounds() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("compare");
+        table.endHeaders();
+        RestTable.TableIndexComparator comparator = new RestTable.TableIndexComparator(table,
+            Collections.singletonList(new RestTable.ColumnOrderElement("compare", false)));
+        Error e = expectThrows(AssertionError.class, () -> {
+            comparator.compare(0,1);
+        });
+        assertEquals("Invalid comparison of indices (0, 1): Table has 0 rows.", e.getMessage());
+    }
+
+    public void testUnknownHeader() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("compare");
+        table.endHeaders();
+        restRequest.params().put("s", "notaheader");
+        Exception e = expectThrows(UnsupportedOperationException.class, () -> RestTable.getRowOrder(table, restRequest));
+        assertEquals("Unable to sort by unknown sort key `notaheader`", e.getMessage());
+    }
+
+    public void testAliasSort() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("compare", "alias:c;");
+        table.endHeaders();
+        List<Integer> comparisonList = Arrays.asList(3,1,2);
+        for (int i = 0; i < comparisonList.size(); i++) {
+            table.startRow();
+            table.addCell(comparisonList.get(i));
+            table.endRow();
+        }
+        restRequest.params().put("s", "c");
+        List<Integer> rowOrder = RestTable.getRowOrder(table, restRequest);
+        assertEquals(Arrays.asList(1,2,0), rowOrder);
+    }
+
+    public void testReversedSort() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("reversed");
+        table.endHeaders();
+        List<Integer> comparisonList = Arrays.asList(0, 1, 2);
+        for (int i = 0; i < comparisonList.size(); i++) {
+            table.startRow();
+            table.addCell(comparisonList.get(i));
+            table.endRow();
+        }
+        restRequest.params().put("s", "reversed:desc");
+        List<Integer> rowOrder = RestTable.getRowOrder(table, restRequest);
+        assertEquals(Arrays.asList(2,1,0), rowOrder);
+    }
+
+    public void testMultiSort() {
+        Table table = new Table();
+        table.startHeaders();
+        table.addCell("compare");
+        table.addCell("second.compare");
+        table.endHeaders();
+        List<Integer> comparisonList = Arrays.asList(3, 3, 2);
+        List<Integer> secondComparisonList = Arrays.asList(2, 1, 3);
+        for (int i = 0; i < comparisonList.size(); i++) {
+            table.startRow();
+            table.addCell(comparisonList.get(i));
+            table.addCell(secondComparisonList.get(i));
+            table.endRow();
+        }
+        restRequest.params().put("s", "compare,second.compare");
+        List<Integer> rowOrder = RestTable.getRowOrder(table, restRequest);
+        assertEquals(Arrays.asList(2,1,0), rowOrder);
+
+        restRequest.params().put("s", "compare:desc,second.compare");
+        rowOrder = RestTable.getRowOrder(table, restRequest);
+        assertEquals(Arrays.asList(1,0,2), rowOrder);
+    }
+
     private RestResponse assertResponseContentType(Map<String, String> headers, String mediaType) throws Exception {
         FakeRestRequest requestWithAcceptHeader = new FakeRestRequest.Builder().withHeaders(headers).build();
         table.startRow();

+ 30 - 0
docs/reference/cat.asciidoc

@@ -175,6 +175,36 @@ For example:
 --------------------------------------------------
 // NOTCONSOLE
 
+[float]
+[[sort]]
+=== Sort
+
+Each of the commands accepts a query string parameter `s` which sorts the table by
+the columns specified as the parameter value. Columns are specified either by name or by
+alias, and are provided as a comma separated string. By default, sorting is done in
+ascending fashion. Appending `:desc` to a column will invert the ordering for
+that column. `:asc` is also accepted but exhibits the same behavior as the default sort order.
+
+For example, with a sort string `s=column1,column2:desc,column3`, the table will be
+sorted in ascending order by column1, in descending order by column2, and in ascending
+order by column3.
+
+[source,sh]
+--------------------------------------------------
+GET _cat/templates?v&s=order:desc,template
+--------------------------------------------------
+//CONSOLE
+
+returns:
+
+[source,sh]
+--------------------------------------------------
+name                  template     order version
+pizza_pepperoni       *pepperoni*  2
+sushi_california_roll *avocado*    1     1
+pizza_hawaiian        *pineapples* 1
+--------------------------------------------------
+
 --
 
 include::cat/alias.asciidoc[]

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.aliases.json

@@ -33,6 +33,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.allocation.json

@@ -38,6 +38,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.count.json

@@ -33,6 +33,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.fielddata.json

@@ -38,6 +38,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.health.json

@@ -29,6 +29,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "ts": {
           "type": "boolean",
           "description": "Set to false to disable timestamping",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.help.json

@@ -12,6 +12,10 @@
           "type": "boolean",
           "description": "Return help information",
           "default": false
+        },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
         }
       }
     },

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json

@@ -49,6 +49,10 @@
           "description": "Set to true to return stats only for primary shards",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.master.json

@@ -29,6 +29,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodeattrs.json

@@ -29,6 +29,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json

@@ -29,6 +29,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.pending_tasks.json

@@ -29,6 +29,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.plugins.json

@@ -27,6 +27,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.recovery.json

@@ -34,6 +34,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.repositories.json

@@ -30,6 +30,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.segments.json

@@ -25,6 +25,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.shards.json

@@ -33,6 +33,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.snapshots.json

@@ -37,6 +37,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.tasks.json

@@ -41,6 +41,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.templates.json

@@ -33,6 +33,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/cat.thread_pool.json

@@ -34,6 +34,10 @@
           "description": "Return help information",
           "default": false
         },
+        "s": {
+          "type": "list",
+          "description" : "Comma-separated list of column names or column aliases to sort by"
+        },
         "v": {
           "type": "boolean",
           "description": "Verbose mode. Display column headers",

+ 43 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/cat.aliases/10_basic.yaml

@@ -216,3 +216,46 @@
                     -                   \s+
                 $/
 
+---
+"Alias sorting":
+
+  - do:
+      indices.create:
+        index: test_index
+        body:
+          aliases:
+            test_alias: {}
+            my_alias: {}
+
+  - do:
+      indices.create:
+        index: other_index
+        body:
+          aliases:
+            other_alias: {}
+
+  - do:
+      cat.aliases:
+        h:      [alias, index]
+        s:      [index, alias]
+
+  - match:
+      $body: |
+            /^
+                other_alias \s+ other_index\n
+                my_alias \s+ test_index\n
+                test_alias \s+ test_index\n
+            $/
+
+  - do:
+      cat.aliases:
+        h:      [alias, index]
+        s:      [index, "a:desc"]
+
+  - match:
+      $body: |
+            /^
+                other_alias \s+ other_index\n
+                test_alias \s+ test_index\n
+                my_alias \s+ test_index\n
+            $/

+ 54 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/cat.indices/10_basic.yaml

@@ -157,3 +157,57 @@
   - match:
       $body: |
                /^(ba(r|z) \n?){2}$/
+
+---
+"Test cat indices sort":
+  - do:
+      indices.create:
+        index: foo
+        body:
+          settings:
+            number_of_shards: "1"
+            number_of_replicas: "0"
+
+  - do:
+      indices.create:
+        index: bar
+        body:
+          settings:
+            number_of_shards: "1"
+            number_of_replicas: "0"
+
+  - do:
+      indices.create:
+        index: baz
+        body:
+          settings:
+            number_of_shards: "1"
+            number_of_replicas: "0"
+
+  - do:
+      indices.close:
+        index: bar
+
+  - do:
+      cat.indices:
+        h: [status, index]
+        s: [status, index]
+
+  - match:
+      $body: |
+            /^ close \s+ bar\n
+               open \s+ baz\n
+               open \s+ foo\n
+            $/
+
+  - do:
+      cat.indices:
+        h: [status, index]
+        s: [status, "index:desc"]
+
+  - match:
+      $body: |
+            /^ close \s+ bar\n
+               open \s+ foo\n
+               open \s+ baz\n
+            $/

+ 39 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/cat.repositories/10_basic.yaml

@@ -43,3 +43,42 @@
                /^   test_cat_repo_1\s+ fs\s*\n
                     test_cat_repo_2\s+ fs\s*\n
                $/
+
+---
+"Test cat repositories sort":
+
+  - do:
+      snapshot.create_repository:
+        repository: test_cat_repo_1
+        body:
+          type: fs
+          settings:
+            location: "test_cat_repo_1_loc"
+
+  - do:
+      snapshot.create_repository:
+        repository: test_cat_repo_2
+        body:
+          type: fs
+          settings:
+            location: "test_cat_repo_2_loc"
+
+  - do:
+      cat.repositories:
+        s: [type, id]
+
+  - match:
+      $body: |
+             /^ test_cat_repo_1 \s+ fs \n
+                test_cat_repo_2 \s+ fs \n
+             $/
+
+  - do:
+      cat.repositories:
+        s: [type, "id:desc"]
+
+  - match:
+      $body: |
+             /^ test_cat_repo_2 \s+ fs \n
+                test_cat_repo_1 \s+ fs \n
+             $/

+ 37 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/cat.shards/10_basic.yaml

@@ -224,3 +224,40 @@
   - match:
       $body: |
                /^(ba(r|z) \n?){2}$/
+
+---
+"Test cat shards sort":
+
+  - do:
+      indices.create:
+        index: foo
+        body:
+          settings:
+            number_of_shards: "1"
+            number_of_replicas: "0"
+
+  - do:
+      indices.create:
+        index: bar
+        body:
+          settings:
+            number_of_shards: "1"
+            number_of_replicas: "0"
+
+  - do:
+      index:
+        index: bar
+        type: type
+        body: { test: bar }
+        refresh: true
+
+  - do:
+      cat.shards:
+        h: [index, store]
+        s: [store]
+
+  - match:
+      $body: |
+            /^ foo \s+ (\d+|\d+[.]\d+)(kb|b)\n
+               bar \s+ (\d+|\d+[.]\d+)(kb|b)\n
+            $/

+ 47 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/cat.templates/10_basic.yaml

@@ -174,3 +174,50 @@
                     \n
                 $/
 
+---
+"Sort templates":
+
+    - do:
+        indices.put_template:
+          name: test
+          body:
+            order: 0
+            template: t*
+            settings:
+              number_of_shards: 1
+              number_of_replicas: 0
+
+    - do:
+        indices.put_template:
+          name: test_1
+          body:
+            order: 0
+            version: 1
+            template: te*
+            settings:
+              number_of_shards: 1
+              number_of_replicas: 0
+
+    - do:
+        cat.templates:
+            h: [name, template, version]
+            s: [version]
+
+    - match:
+        $body: |
+              /^
+                  test \s+ t\* \s+\n
+                  test_1 \s+ te\* \s+ 1\n
+              $/
+
+    - do:
+        cat.templates:
+            h: [name, template, version]
+            s: ["version:desc"]
+
+    - match:
+        $body: |
+              /^
+                  test_1 \s+ te\* \s+ 1\n
+                  test \s+ t\* \s+\n
+              $/