diff --git a/core/src/main/java/org/elasticsearch/common/Table.java b/core/src/main/java/org/elasticsearch/common/Table.java index ab0252b11dc..430070ee19c 100644 --- a/core/src/main/java/org/elasticsearch/common/Table.java +++ b/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 headers = new ArrayList<>(); @@ -197,6 +195,22 @@ public class Table { return null; } + public Map getAliasMap() { + Map 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 attr; diff --git a/core/src/main/java/org/elasticsearch/common/unit/ByteSizeUnit.java b/core/src/main/java/org/elasticsearch/common/unit/ByteSizeUnit.java index 7a412aac090..e7e43b6d78a 100644 --- a/core/src/main/java/org/elasticsearch/common/unit/ByteSizeUnit.java +++ b/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()); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/elasticsearch/common/unit/ByteSizeValue.java b/core/src/main/java/org/elasticsearch/common/unit/ByteSizeValue.java index 7d2be6fee3e..e0782e32cae 100644 --- a/core/src/main/java/org/elasticsearch/common/unit/ByteSizeValue.java +++ b/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 { 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); } } diff --git a/core/src/main/java/org/elasticsearch/common/unit/SizeValue.java b/core/src/main/java/org/elasticsearch/common/unit/SizeValue.java index cba51f29eeb..0f90582007b 100644 --- a/core/src/main/java/org/elasticsearch/common/unit/SizeValue.java +++ b/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 { 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); } } diff --git a/core/src/main/java/org/elasticsearch/common/unit/TimeValue.java b/core/src/main/java/org/elasticsearch/common/unit/TimeValue.java index 8f81efb6498..4ab91aac5b5 100644 --- a/core/src/main/java/org/elasticsearch/common/unit/TimeValue.java +++ b/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 { /** 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); + } } diff --git a/core/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java b/core/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java index 61932ad1443..0ab2c86453e 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java +++ b/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 displayHeaders = buildDisplayHeaders(table, request); builder.startArray(); - for (int row = 0; row < table.getRows().size(); row++) { + List 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 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 getRowOrder(Table table, RestRequest request) { + String[] columnOrdering = request.paramAsStringArray("s", null); + + List rowOrder = new ArrayList<>(); + for (int i = 0; i < table.getRows().size(); i++) { + rowOrder.add(i); + } + + if (columnOrdering != null) { + Map headerAliasMap = table.getAliasMap(); + List 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 buildDisplayHeaders(Table table, RestRequest request) { List display = new ArrayList<>(); if (request.hasParam("h")) { @@ -368,4 +407,71 @@ public class RestTable { this.display = display; } } + + static class TableIndexComparator implements Comparator { + private final Table table; + private final int maxIndex; + private final List ordering; + + TableIndexComparator(Table table, List 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> 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; + } + } } diff --git a/core/src/test/java/org/elasticsearch/common/TableTests.java b/core/src/test/java/org/elasticsearch/common/TableTests.java index 7e624cb749d..39b5801d7ff 100644 --- a/core/src/test/java/org/elasticsearch/common/TableTests.java +++ b/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 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(); diff --git a/core/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java b/core/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java index 5296e226faa..b01e3f60771 100644 --- a/core/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java +++ b/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()); } } } diff --git a/core/src/test/java/org/elasticsearch/common/unit/SizeValueTests.java b/core/src/test/java/org/elasticsearch/common/unit/SizeValueTests.java index b5fc54de7d0..c109b8869d2 100644 --- a/core/src/test/java/org/elasticsearch/common/unit/SizeValueTests.java +++ b/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()); + } } diff --git a/core/src/test/java/org/elasticsearch/common/unit/TimeValueTests.java b/core/src/test/java/org/elasticsearch/common/unit/TimeValueTests.java index 4d0ac5257a3..1f7e876f856 100644 --- a/core/src/test/java/org/elasticsearch/common/unit/TimeValueTests.java +++ b/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()); + } } diff --git a/core/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java b/core/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java index f7dc6149052..ba3ec55ab2a 100644 --- a/core/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java +++ b/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 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 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 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 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 comparisonList = Arrays.asList(3, 3, 2); + List 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 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 headers, String mediaType) throws Exception { FakeRestRequest requestWithAcceptHeader = new FakeRestRequest.Builder().withHeaders(headers).build(); table.startRow(); diff --git a/docs/reference/cat.asciidoc b/docs/reference/cat.asciidoc index a611cdc97f9..e037907435d 100644 --- a/docs/reference/cat.asciidoc +++ b/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[] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.aliases.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.aliases.json index 4bc93460b75..d30c6ace6f7 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.aliases.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.allocation.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.allocation.json index 6799a67bae8..7c826890607 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.allocation.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.count.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.count.json index 73803bac867..4311d9a3be1 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.count.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.fielddata.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.fielddata.json index b9776ab5881..88c7eee1262 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.fielddata.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.health.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.health.json index 349bf37aa06..e858e83b465 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.health.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.help.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.help.json index f21e6485d58..e893fb1b044 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.help.json +++ b/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" } } }, diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json index 03b67fd14c2..42a47253de7 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.indices.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.master.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.master.json index bdf474c7a02..ab87b2adb7e 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.master.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodeattrs.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodeattrs.json index 4e44eb820f1..3d8a4a77a77 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodeattrs.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json index f0fcf390102..c4d0dcd5f49 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.nodes.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.pending_tasks.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.pending_tasks.json index 74fc54b9604..983b82482ae 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.pending_tasks.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.plugins.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.plugins.json index 5822f64c8e2..93c7feababa 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.plugins.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.recovery.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.recovery.json index b220a5eda74..42f91cedfdd 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.recovery.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.repositories.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.repositories.json index ace0a1ea3c9..c640a568fde 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.repositories.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.segments.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.segments.json index eed4a627736..118f8b6bf96 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.segments.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.shards.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.shards.json index a9a1a30770f..db46ce909ff 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.shards.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.snapshots.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.snapshots.json index dc743893b5f..90f4ca32730 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.snapshots.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.tasks.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.tasks.json index 73f1281ff5f..d7b7acd5a4f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.tasks.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.templates.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.templates.json index f8aaa72723a..f0757c2d652 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.templates.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.thread_pool.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cat.thread_pool.json index d6ba5ebc6b1..0a16f176613 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cat.thread_pool.json +++ b/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", diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.aliases/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/cat.aliases/10_basic.yaml index 770c5c8b441..63670061b6d 100755 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.aliases/10_basic.yaml +++ b/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 + $/ diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.indices/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/cat.indices/10_basic.yaml index 24619e53353..0b3cdba46a4 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.indices/10_basic.yaml +++ b/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 + $/ diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.repositories/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/cat.repositories/10_basic.yaml index c7eb9c1f930..2345df9732e 100755 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.repositories/10_basic.yaml +++ b/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 + $/ diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.shards/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/cat.shards/10_basic.yaml index a95314c57d5..fc077ba6529 100755 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.shards/10_basic.yaml +++ b/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 + $/ diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.templates/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/cat.templates/10_basic.yaml index 6758bec39da..28a7e1f3bee 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/cat.templates/10_basic.yaml +++ b/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 + $/