From 522c136e3168e8578b7f05a80d945a5ea457a7c3 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Wed, 20 May 2026 20:14:54 +0200 Subject: [PATCH] make ComparableExpressions sortable --- api/src/main/java/jakarta/data/Order.java | 3 +- api/src/main/java/jakarta/data/Sort.java | 304 +++++++++++++++--- api/src/main/java/jakarta/data/Sortable.java | 47 +++ .../data/expression/ComparableExpression.java | 3 +- .../data/metamodel/SortableAttribute.java | 24 +- .../jakarta/data/metamodel/TextAttribute.java | 24 +- api/src/test/java/jakarta/data/SortTest.java | 8 +- 7 files changed, 319 insertions(+), 94 deletions(-) create mode 100644 api/src/main/java/jakarta/data/Sortable.java diff --git a/api/src/main/java/jakarta/data/Order.java b/api/src/main/java/jakarta/data/Order.java index b7772db36..99c0fb8b6 100644 --- a/api/src/main/java/jakarta/data/Order.java +++ b/api/src/main/java/jakarta/data/Order.java @@ -140,7 +140,7 @@ public List> sorts() { @Override public boolean equals(Object other) { return this == other - || other instanceof Order s && sorts.equals(s.sorts); + || other instanceof Order s && sorts.equals(s.sorts); } /** @@ -164,7 +164,6 @@ public Iterator> iterator() { return sorts.iterator(); } - /** * Textual representation of this instance, including the result of invoking * {@link Sort#toString()} on each member of the sort criteria, in order of diff --git a/api/src/main/java/jakarta/data/Sort.java b/api/src/main/java/jakarta/data/Sort.java index 004c217ad..1a69bb145 100644 --- a/api/src/main/java/jakarta/data/Sort.java +++ b/api/src/main/java/jakarta/data/Sort.java @@ -18,9 +18,13 @@ package jakarta.data; import jakarta.data.messages.Messages; +import jakarta.data.metamodel.Attribute; import jakarta.data.metamodel.StaticMetamodel; +import jakarta.data.metamodel.TextAttribute; import jakarta.data.repository.OrderBy; +import java.util.Objects; + /** *

Requests sorting on a given entity attribute.

* @@ -78,20 +82,14 @@ * if the database is incapable of ordering the query results using the given * sort criteria.

* - * @param entity class of the entity attribute upon which to sort. - * @param property name of the entity attribute to order by. - * @param isAscending whether ordering for this attribute is ascending - * ({@code true}) or descending ({@code false}). - * @param ignoreCase whether or not to request case insensitive ordering - * from a database with case sensitive collation. - * @param nullOrdering whether {@code null} values are ordered - * {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, or - * {@linkplain Nulls#UNSPECIFIED by the data store}. */ -public record Sort(String property, - boolean isAscending, - boolean ignoreCase, - Nulls nullOrdering) { +public final class Sort { + + private final Sortable sortable; + private final String property; + private final boolean isAscending; + private final boolean ignoreCase; + private final Nulls nullOrdering; /** * Indicates how {@code null} values are ordered. @@ -123,42 +121,51 @@ public enum Nulls { } /** - *

Defines sort criteria for an entity attribute. For more descriptive - * code, use:

- *
    - *
  • {@link #asc(String) Sort.asc(attributeName)} - * for ascending sort on an entity attribute.
  • - *
  • {@link #asc(String) Sort.asc(attributeName)}.{@link #nullsFirst()} - * for ascending sort on an entity attribute, - * where {@code null} values are ordered first.
  • - *
  • {@link #ascIgnoreCase(String) Sort.ascIgnoreCase(attributeName)} - * for case insensitive ascending sort on an entity attribute.
  • - *
  • {@link #ascIgnoreCase(String) Sort.ascIgnoreCase(attributeName)}.{@link #nullsFirst()} - * for case insensitive ascending sort on an entity attribute, - * where {@code null} values are ordered first.
  • - *
  • {@link #desc(String) Sort.desc(attributeName)} - * for descending sort on an entity attribute.
  • - *
  • {@link #desc(String) Sort.desc(attributeName)}.{@link #nullsLast()} - * for descending sort on an entity attribute, - * where {@code null} values are ordered last.
  • - *
  • {@link #descIgnoreCase(String) Sort.descIgnoreCase(attributeName)} - * for case insensitive descending sort on an entity attribute.
  • - *
  • {@link #descIgnoreCase(String) Sort.descIgnoreCase(attributeName)}.{@link #nullsLast()} - * for case insensitive descending sort on an entity attribute, - * where {@code null} values are ordered last.
  • - *
+ * Construct a sorting criterion for the given + * {@linkplain Sortable sortable entity attribute or sortable expression}. + * + * @param sortable the sortable entity attribute or sortable expression. + * @param isAscending whether ordering for this attribute is ascending + * (true) or descending (false). + * @param ignoreCase whether or not to request case-insensitive ordering + * from a database with case-sensitive collation. + * @param nullOrdering whether {@code null} values are ordered + * {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, + * or {@linkplain Nulls#UNSPECIFIED by the data store}. + */ + private Sort(Sortable sortable, boolean isAscending, boolean ignoreCase, Nulls nullOrdering) { + if (sortable == null) { + throw new NullPointerException( + Messages.get("001.arg.required", "sortable")); + } + + if (nullOrdering == null) { + throw new NullPointerException( + Messages.get("001.arg.required", "nullOrdering")); + } + this.sortable = sortable; + this.property = + sortable instanceof Attribute attribute + ? attribute.name() + : null; + this.isAscending = isAscending; + this.ignoreCase = ignoreCase; + this.nullOrdering = nullOrdering; + } + + /** + * Construct a sorting criterion for a named entity attribute. * * @param property name of the entity attribute to order by. * @param isAscending whether ordering for this attribute is ascending * (true) or descending (false). - * @param ignoreCase whether or not to request case insensitive ordering - * from a database with case sensitive collation. + * @param ignoreCase whether or not to request case-insensitive ordering + * from a database with case-sensitive collation. * @param nullOrdering whether {@code null} values are ordered * {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, * or {@linkplain Nulls#UNSPECIFIED by the data store}. - * @since 1.1 */ - public Sort { + private Sort(String property, boolean isAscending, boolean ignoreCase, Nulls nullOrdering) { if (property == null) { throw new NullPointerException( Messages.get("001.arg.required", "attribute")); @@ -168,8 +175,15 @@ public enum Nulls { throw new NullPointerException( Messages.get("001.arg.required", "nullOrdering")); } + this.sortable = null; + this.property = property; + this.isAscending = isAscending; + this.ignoreCase = ignoreCase; + this.nullOrdering = nullOrdering; } + // Deprecated public constructor: + /** *

Constructor for compatibility with Jakarta Data 1.0. Use the * {@link #of(String, Direction, boolean)} method instead.

@@ -177,19 +191,59 @@ public enum Nulls { * @param property name of the entity attribute to order by. * @param isAscending whether ordering for this attribute is ascending * ({@code true}) or descending ({@code false}). - * @param ignoreCase whether or not to request case insensitive ordering - * from a database with case sensitive collation. + * @param ignoreCase whether or not to request case-insensitive ordering + * from a database with case-sensitive collation. + * + * @deprecated This class should not be directly instantiated by clients. + * For more descriptive code, use:

+ *
    + *
  • {@link #asc(String) Sort.asc(attributeName)} + * for ascending sort on an entity attribute.
  • + *
  • {@link #asc(String) Sort.asc(attributeName)}.{@link #nullsFirst()} + * for ascending sort on an entity attribute, + * where {@code null} values are ordered first.
  • + *
  • {@link #ascIgnoreCase(String) Sort.ascIgnoreCase(attributeName)} + * for case-insensitive ascending sort on an entity attribute.
  • + *
  • {@link #ascIgnoreCase(String) Sort.ascIgnoreCase(attributeName)}.{@link #nullsFirst()} + * for case-insensitive ascending sort on an entity attribute, + * where {@code null} values are ordered first.
  • + *
  • {@link #desc(String) Sort.desc(attributeName)} + * for descending sort on an entity attribute.
  • + *
  • {@link #desc(String) Sort.desc(attributeName)}.{@link #nullsLast()} + * for descending sort on an entity attribute, + * where {@code null} values are ordered last.
  • + *
  • {@link #descIgnoreCase(String) Sort.descIgnoreCase(attributeName)} + * for case-insensitive descending sort on an entity attribute.
  • + *
  • {@link #descIgnoreCase(String) Sort.descIgnoreCase(attributeName)}.{@link #nullsLast()} + * for case-insensitive descending sort on an entity attribute, + * where {@code null} values are ordered last.
  • + *
+ * This constructor will be removed in a future release. */ + @Deprecated(since = "1.1", forRemoval = true) public Sort(String property, boolean isAscending, boolean ignoreCase) { this(property, isAscending, ignoreCase, Nulls.UNSPECIFIED); } // Override to provide method documentation: + /** + * The sortable entity attribute or sortable expression to + * order by. + * + * @return The sortable entity attribute or sortable expression, + * or {@code null} if this object was instantiated with + * only a {@linkplain #property() property name}. + */ + public Sortable sortable() { + return sortable; + } + /** * Name of the entity attribute to order by. * - * @return The attribute name to order by; will never be {@code null}. + * @return The attribute name to order by or {@code null} + * if the sortable expression is not an attribute. */ public String property() { return property; @@ -198,12 +252,12 @@ public String property() { // Override to provide method documentation: /** - *

Indicates whether or not to request case insensitive ordering - * from a database with case sensitive collation. A database with case - * insensitive collation performs case insensitive ordering regardless of - * the requested {@code ignoreCase} value.

+ *

Indicates whether or not to request case-insensitive ordering + * from a database with case-sensitive collation. A database with + * case-insensitive collation performs case-insensitive ordering + * regardless of the requested {@code ignoreCase} value.

* - * @return Returns whether or not to request case insensitive sorting for + * @return Returns whether or not to request case-insensitive sorting for * the entity attribute. */ public boolean ignoreCase() { @@ -244,13 +298,67 @@ public Nulls nullOrdering() { return nullOrdering; } + /** + * Create a {@link Sort} instance. + * + * @param entity class of the sortable entity attribute. + * @param sortable the sortable entity attribute or sortable expression. + * @param direction the direction in which to order. + * @param ignoreCase whether to request a case-insensitive ordering. + * @return a {@link Sort} instance. Never {@code null}. + * @throws NullPointerException when there is a null parameter. + */ + public static Sort of(Sortable sortable, + Direction direction, + boolean ignoreCase) { + if (direction == null) { + throw new NullPointerException( + Messages.get("001.arg.required", "direction")); + } + + return new Sort<>(sortable, + Direction.ASC.equals(direction), + ignoreCase, + Nulls.UNSPECIFIED); + } + + /** + *

Create a {@link Sort} instance, indicating how {@code null} values + * are ordered.

+ * + * @param entity class of the sortable entity attribute. + * @param sortable the sortable entity attribute or sortable expression. + * @param direction the direction in which to order. + * @param ignoreCase whether to request a case-insensitive ordering. + * @param nullOrdering whether {@code null} values are ordered + * {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, + * or {@linkplain Nulls#UNSPECIFIED by the data store}. + * @return a {@link Sort} instance. Never {@code null}. + * @throws NullPointerException when there is a {@code null} parameter. + * @since 1.1 + */ + public static Sort of(Sortable sortable, + Direction direction, + boolean ignoreCase, + Nulls nullOrdering) { + if (direction == null) { + throw new NullPointerException( + Messages.get("001.arg.required", "direction")); + } + + return new Sort<>(sortable, + Direction.ASC.equals(direction), + ignoreCase, + nullOrdering); + } + /** * Create a {@link Sort} instance. * * @param entity class of the sortable entity attribute. * @param attribute name of the entity attribute to order by. * @param direction the direction in which to order. - * @param ignoreCase whether to request a case insensitive ordering. + * @param ignoreCase whether to request a case-insensitive ordering. * @return a {@link Sort} instance. Never {@code null}. * @throws NullPointerException when there is a null parameter. */ @@ -275,7 +383,7 @@ public static Sort of(String attribute, * @param entity class of the sortable entity attribute. * @param attribute name of the entity attribute to order by. * @param direction the direction in which to order. - * @param ignoreCase whether to request a case insensitive ordering. + * @param ignoreCase whether to request a case-insensitive ordering. * @param nullOrdering whether {@code null} values are ordered * {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, * or {@linkplain Nulls#UNSPECIFIED by the data store}. @@ -298,6 +406,21 @@ public static Sort of(String attribute, nullOrdering); } + /** + * Create a {@link Sort} instance with + * {@linkplain Direction#ASC ascending direction} that does not + * request case-insensitive ordering. + * + * @param entity class of the sortable entity attribute. + * @param sortable the sortable entity attribute or sortable expression. + * @return a {@link Sort} instance. Never {@code null}. + * @throws NullPointerException when the attribute name is null. + * @since 1.1 + */ + public static Sort asc(Sortable sortable) { + return new Sort<>(sortable, true, false, Nulls.UNSPECIFIED); + } + /** * Create a {@link Sort} instance with * {@linkplain Direction#ASC ascending direction} that does not @@ -314,7 +437,23 @@ public static Sort asc(String attribute) { /** * Create a {@link Sort} instance with - * {@link Direction#ASC ascending direction} and case insensitive ordering. + * {@linkplain Direction#ASC ascending direction} and case-insensitive + * ordering. + * + * @param entity class of the sortable entity attribute. + * @param sortable the sortable entity attribute or sortable expression. + * @return a {@link Sort} instance. Never {@code null}. + * @throws NullPointerException when the attribute name is null. + * @since 1.1 + */ + public static Sort ascIgnoreCase(TextAttribute sortable) { + return new Sort<>(sortable, true, true, Nulls.UNSPECIFIED); + } + + /** + * Create a {@link Sort} instance with + * {@linkplain Direction#ASC ascending direction} and case-insensitive + * ordering. * * @param entity class of the sortable entity attribute. * @param attribute name of the entity attribute to order by. @@ -325,6 +464,21 @@ public static Sort ascIgnoreCase(String attribute) { return new Sort<>(attribute, true, true, Nulls.UNSPECIFIED); } + /** + * Create a {@link Sort} instance with + * {@linkplain Direction#DESC descending direction} that does not + * request case-insensitive ordering. + * + * @param entity class of the sortable entity attribute. + * @param sortable the sortable entity attribute or sortable expression. + * @return a {@link Sort} instance. Never {@code null}. + * @throws NullPointerException when the attribute name is null. + * @since 1.1 + */ + public static Sort desc(Sortable sortable) { + return new Sort<>(sortable, false, false, Nulls.UNSPECIFIED); + } + /** * Create a {@link Sort} instance with * {@linkplain Direction#DESC descending direction} that does not @@ -341,7 +495,22 @@ public static Sort desc(String attribute) { /** * Create a {@link Sort} instance with - * {@link Direction#DESC descending direction} and case insensitive + * {@linkplain Direction#DESC descending direction} and case-insensitive + * ordering. + * + * @param entity class of the sortable entity attribute. + * @param sortable the sortable entity attribute or sortable expression. + * @return a {@link Sort} instance. Never {@code null}. + * @throws NullPointerException when the attribute name is null. + * @since 1.1 + */ + public static Sort descIgnoreCase(TextAttribute sortable) { + return new Sort<>(sortable, false, true, Nulls.UNSPECIFIED); + } + + /** + * Create a {@link Sort} instance with + * {@linkplain Direction#DESC descending direction} and case-insensitive * ordering. * * @param entity class of the sortable entity attribute. @@ -400,4 +569,33 @@ public Sort nullsFirst() { public Sort nullsLast() { return new Sort<>(property, isAscending, ignoreCase, Nulls.LAST); } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else { + return obj instanceof Sort that + && Objects.equals(this.property, that.property) + && this.isAscending == that.isAscending + && this.ignoreCase == that.ignoreCase + && Objects.equals(this.nullOrdering, that.nullOrdering); + } + } + + @Override + public int hashCode() { + return Objects.hash(property, isAscending, ignoreCase, nullOrdering); + } + + @Override + public String toString() { + //TODO: + return "Sort[" + + "property=" + property + ", " + + "isAscending=" + isAscending + ", " + + "ignoreCase=" + ignoreCase + ", " + + "nullOrdering=" + nullOrdering + ']'; + } + } \ No newline at end of file diff --git a/api/src/main/java/jakarta/data/Sortable.java b/api/src/main/java/jakarta/data/Sortable.java new file mode 100644 index 000000000..754411c42 --- /dev/null +++ b/api/src/main/java/jakarta/data/Sortable.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +/** + * An entity attribute or expression that can be used to sort query results. + * + * @since 1.1 + * @param the entity type + */ +public interface Sortable { + + /** + * Obtain an ascending {@linkplain Sort sorting criterion} based on + * this entity attribute or expression. + * + * @return the sorting criterion. + */ + default Sort asc() { + return Sort.asc(this); + } + + /** + * Obtain a descending {@linkplain Sort sorting criterion} based on + * this entity attribute or expression. + * + * @return the sorting criterion. + */ + default Sort desc() { + return Sort.desc(this); + } +} diff --git a/api/src/main/java/jakarta/data/expression/ComparableExpression.java b/api/src/main/java/jakarta/data/expression/ComparableExpression.java index e374bf9a5..f85af8064 100644 --- a/api/src/main/java/jakarta/data/expression/ComparableExpression.java +++ b/api/src/main/java/jakarta/data/expression/ComparableExpression.java @@ -17,6 +17,7 @@ */ package jakarta.data.expression; +import jakarta.data.Sortable; import jakarta.data.constraint.Between; import jakarta.data.constraint.GreaterThan; import jakarta.data.constraint.AtLeast; @@ -44,7 +45,7 @@ * @since 1.1 */ public interface ComparableExpression> - extends Expression { + extends Expression, Sortable { /** *

Obtains a {@link Restriction} that requires that this expression diff --git a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java index 68e20b146..3e808a92f 100644 --- a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java +++ b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java @@ -17,7 +17,7 @@ */ package jakarta.data.metamodel; -import jakarta.data.Sort; +import jakarta.data.Sortable; import jakarta.data.messages.Messages; /** @@ -38,27 +38,7 @@ * * @param entity class of the static metamodel. */ -public interface SortableAttribute extends Attribute { - - /** - * Obtain a request for an ascending {@link Sort} based on the entity - * attribute. - * - * @return a request for an ascending sort on the entity attribute. - */ - default Sort asc() { - return Sort.asc(name()); - } - - /** - * Obtain a request for a descending {@link Sort} based on the entity - * attribute. - * - * @return a request for a descending sort on the entity attribute. - */ - default Sort desc() { - return Sort.desc(name()); - } +public interface SortableAttribute extends Attribute, Sortable { /** *

Creates a static metamodel {@code SortableAttribute} representing the diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java index 0eeb7c4aa..d4c53d342 100644 --- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java +++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java @@ -28,17 +28,6 @@ */ public interface TextAttribute extends ComparableAttribute, TextExpression { - /** - * Obtain a request for an ascending, case-insensitive {@link Sort} based on - * the entity attribute. - * - * @return a request for an ascending, case-insensitive sort on the entity - * attribute. - */ - default Sort ascIgnoreCase() { - return Sort.ascIgnoreCase(name()); - } - /** * Returns {@code String.class} as the entity attribute type for text * attributes. @@ -51,6 +40,17 @@ default Class type() { return String.class; } + /** + * Obtain a request for an ascending, case-insensitive {@link Sort} based on + * the entity attribute. + * + * @return a request for an ascending, case-insensitive sort on the entity + * attribute. + */ + default Sort ascIgnoreCase() { + return Sort.ascIgnoreCase(this); + } + /** * Obtain a request for a descending, case insensitive {@link Sort} based on * the entity attribute. @@ -59,7 +59,7 @@ default Class type() { * attribute. */ default Sort descIgnoreCase() { - return Sort.descIgnoreCase(name()); + return Sort.descIgnoreCase(this); } /** diff --git a/api/src/test/java/jakarta/data/SortTest.java b/api/src/test/java/jakarta/data/SortTest.java index ea7facf60..8b1a90c4b 100644 --- a/api/src/test/java/jakarta/data/SortTest.java +++ b/api/src/test/java/jakarta/data/SortTest.java @@ -34,17 +34,17 @@ class SortTest { @DisplayName("Should throw NullPointerException when one of the properties are null") void shouldReturnErrorWhenPropertyDirectionNull() { assertThatNullPointerException().isThrownBy(() -> - Sort.of(null, null, false)); + Sort.of((String) null, null, false)); assertThatNullPointerException().isThrownBy(() -> Sort.of(NAME, null, true)); assertThatNullPointerException().isThrownBy(() -> - Sort.of(null, Direction.ASC, false)); + Sort.of((String) null, Direction.ASC, false)); assertThatNullPointerException().isThrownBy(() -> - Sort.of(null, null, false, Sort.Nulls.FIRST)); + Sort.of((String) null, null, false, Sort.Nulls.FIRST)); assertThatNullPointerException().isThrownBy(() -> Sort.of(NAME, null, true, Sort.Nulls.LAST)); assertThatNullPointerException().isThrownBy(() -> - Sort.of(null, Direction.ASC, false, Sort.Nulls.UNSPECIFIED)); + Sort.of((String) null, Direction.ASC, false, Sort.Nulls.UNSPECIFIED)); assertThatNullPointerException().isThrownBy(() -> Sort.of(NAME, Direction.ASC, false, null)); }