diff --git a/src/main/java/de/metas/ui/web/window/model/DocumentQueryOrderBy.java b/src/main/java/de/metas/ui/web/window/model/DocumentQueryOrderBy.java index 6c1203593..1757c418d 100644 --- a/src/main/java/de/metas/ui/web/window/model/DocumentQueryOrderBy.java +++ b/src/main/java/de/metas/ui/web/window/model/DocumentQueryOrderBy.java @@ -3,7 +3,6 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; import org.adempiere.util.Check; @@ -12,6 +11,8 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; +import lombok.Builder; +import lombok.ToString; import lombok.Value; /* @@ -39,21 +40,17 @@ @Value public final class DocumentQueryOrderBy { - public static final DocumentQueryOrderBy byFieldName(final String fieldName, final boolean ascending) - { - return new DocumentQueryOrderBy(fieldName, ascending); - } - public static final DocumentQueryOrderBy byFieldName(final String fieldName) { final boolean ascending = true; - return new DocumentQueryOrderBy(fieldName, ascending); + final boolean nullsLast = getDefaultNullsLastByAscending(ascending); + return new DocumentQueryOrderBy(fieldName, ascending, nullsLast); } - public static final DocumentQueryOrderBy byFieldNameDescending(final String fieldName) + public static final DocumentQueryOrderBy byFieldName(final String fieldName, final boolean ascending) { - final boolean ascending = false; - return new DocumentQueryOrderBy(fieldName, ascending); + final boolean nullsLast = getDefaultNullsLastByAscending(ascending); + return new DocumentQueryOrderBy(fieldName, ascending, nullsLast); } /** @@ -83,28 +80,42 @@ private static final DocumentQueryOrderBy parseOrderBy(final String orderByStr) if (orderByStr.charAt(0) == '+') { final String fieldName = orderByStr.substring(1); - return DocumentQueryOrderBy.byFieldName(fieldName, true); + final boolean ascending = true; + final boolean nullsLast = getDefaultNullsLastByAscending(ascending); + return new DocumentQueryOrderBy(fieldName, ascending, nullsLast); } else if (orderByStr.charAt(0) == '-') { final String fieldName = orderByStr.substring(1); - return DocumentQueryOrderBy.byFieldName(fieldName, false); + final boolean ascending = false; + final boolean nullsLast = getDefaultNullsLastByAscending(ascending); + return new DocumentQueryOrderBy(fieldName, ascending, nullsLast); } else { final String fieldName = orderByStr; - return DocumentQueryOrderBy.byFieldName(fieldName, true); + final boolean ascending = true; + final boolean nullsLast = getDefaultNullsLastByAscending(ascending); + return new DocumentQueryOrderBy(fieldName, ascending, nullsLast); } } + private static final boolean getDefaultNullsLastByAscending(final boolean ascending) + { + return true; // always nulls last + } + private final String fieldName; private final boolean ascending; + private final boolean nullsLast; - private DocumentQueryOrderBy(final String fieldName, final boolean ascending) + @Builder + private DocumentQueryOrderBy(final String fieldName, final Boolean ascending, final Boolean nullsLast) { Check.assumeNotEmpty(fieldName, "fieldName is not empty"); this.fieldName = fieldName; - this.ascending = ascending; + this.ascending = ascending != null ? ascending : true; + this.nullsLast = nullsLast != null ? nullsLast : getDefaultNullsLastByAscending(this.ascending); } public DocumentQueryOrderBy copyOverridingFieldName(final String fieldName) @@ -113,53 +124,76 @@ public DocumentQueryOrderBy copyOverridingFieldName(final String fieldName) { return this; } - return new DocumentQueryOrderBy(fieldName, ascending); + return new DocumentQueryOrderBy(fieldName, ascending, nullsLast); } - public Comparator asComparator(final BiFunction fieldValueExtractor) + public Comparator asComparator(final FieldValueExtractor fieldValueExtractor) { - final Function keyExtractor = obj -> fieldValueExtractor.apply(obj, fieldName); - Comparator cmp = Comparator.comparing(keyExtractor, ValueComparator.instance); - - if (!ascending) - { - cmp = cmp.reversed(); - } + final Function keyExtractor = obj -> fieldValueExtractor.getFieldValue(obj, fieldName); + Comparator keyComparator = ValueComparator.ofAscendingAndNullsLast(ascending, nullsLast); + return Comparator.comparing(keyExtractor, keyComparator); + } - return cmp; + @FunctionalInterface + public static interface FieldValueExtractor + { + Object getFieldValue(T object, String fieldName); } + @ToString private static final class ValueComparator implements Comparator { - public static final transient ValueComparator instance = new ValueComparator(); + public static final ValueComparator ofAscendingAndNullsLast(final boolean ascending, final boolean nullsLast) + { + if (ascending) + { + return nullsLast ? ASCENDING_NULLS_LAST : ASCENDING_NULLS_FIRST; + } + else + { + return nullsLast ? DESCENDING_NULLS_LAST : DESCENDING_NULLS_FIRST; + } + } + + public static final transient ValueComparator ASCENDING_NULLS_FIRST = new ValueComparator(true, false); + public static final transient ValueComparator ASCENDING_NULLS_LAST = new ValueComparator(true, true); + public static final transient ValueComparator DESCENDING_NULLS_FIRST = new ValueComparator(false, false); + public static final transient ValueComparator DESCENDING_NULLS_LAST = new ValueComparator(false, true); + + private final boolean ascending; + private final boolean nullsLast; - private ValueComparator() + private ValueComparator(final boolean ascending, final boolean nullsLast) { - super(); + this.ascending = ascending; + this.nullsLast = nullsLast; } @Override public int compare(final Object o1, final Object o2) { - if (o1 instanceof Comparable) + if (o1 == o2) { - @SuppressWarnings("unchecked") - final Comparable o1cmp = (Comparable)o1; - return o1cmp.compareTo(o2); + return 0; } else if (o1 == null) { - return o2 == null ? 0 : -1; + return nullsLast ? +1 : -1; } else if (o2 == null) { - return +1; + return nullsLast ? -1 : +1; + } + else if (o1 instanceof Comparable) + { + @SuppressWarnings("unchecked") + final Comparable o1cmp = (Comparable)o1; + return o1cmp.compareTo(o2) * (ascending ? +1 : -1); } else { - return o1.toString().compareTo(o2.toString()); + return o1.toString().compareTo(o2.toString()) * (ascending ? +1 : -1); } } - } } diff --git a/src/main/java/de/metas/ui/web/window/model/sql/SqlDocumentOrderByBuilder.java b/src/main/java/de/metas/ui/web/window/model/sql/SqlDocumentOrderByBuilder.java index cf270a91f..8499f6d2a 100644 --- a/src/main/java/de/metas/ui/web/window/model/sql/SqlDocumentOrderByBuilder.java +++ b/src/main/java/de/metas/ui/web/window/model/sql/SqlDocumentOrderByBuilder.java @@ -77,7 +77,7 @@ private final IStringExpression buildSqlOrderBy(final DocumentQueryOrderBy order { final String fieldName = orderBy.getFieldName(); final IStringExpression sqlExpression = bindings.getFieldOrderBy(fieldName); - return SqlDocumentOrderByBuilder.buildSqlOrderBy(sqlExpression, orderBy.isAscending()); + return SqlDocumentOrderByBuilder.buildSqlOrderBy(sqlExpression, orderBy.isAscending(), orderBy.isNullsLast()); } /** @@ -86,7 +86,7 @@ private final IStringExpression buildSqlOrderBy(final DocumentQueryOrderBy order * @param ascending * @return ORDER BY SQL or empty */ - private static final IStringExpression buildSqlOrderBy(final IStringExpression sqlExpression, final boolean ascending) + private static final IStringExpression buildSqlOrderBy(final IStringExpression sqlExpression, final boolean ascending, final boolean nullsLast) { if (sqlExpression.isNullExpression()) { @@ -94,7 +94,9 @@ private static final IStringExpression buildSqlOrderBy(final IStringExpression s } return IStringExpression.composer() - .append("(").append(sqlExpression).append(")").append(ascending ? " ASC" : " DESC") + .append("(").append(sqlExpression).append(")") + .append(ascending ? " ASC" : " DESC") + .append(nullsLast ? " NULLS LAST" : " NULLS FIRST") .build(); }