Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 66 additions & 20 deletions api/src/main/java/jakarta/data/Sort.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
*/
package jakarta.data;

import jakarta.data.expression.ComparableExpression;
import jakarta.data.messages.Messages;
import jakarta.data.metamodel.Attribute;
import jakarta.data.metamodel.StaticMetamodel;
import jakarta.data.repository.OrderBy;

/**
* <p>Requests sorting on a given entity attribute.</p>
* <p>Requests sorting on a given entity attribute or expression.</p>
*
* <p>An instance of {@code Sort} specifies a sorting criterion based
* on an entity attribute, with a sorting {@linkplain Direction direction} and
* well-defined case sensitivity.</p>
* on an entity attribute or an expression involving entity attributes,
* with a sorting {@linkplain Direction direction} and well-defined
* case sensitivity.</p>
*
* <p>A query method of a repository may have a parameter or parameters
* of type {@code Sort} if its return type indicates that it may return multiple
Expand Down Expand Up @@ -78,8 +81,13 @@
* if the database is incapable of ordering the query results using the given
* sort criteria.</p>
*
* @param <T> entity class of the entity attribute upon which to sort.
* @param property name of the entity attribute to order by.
* @param <T> type of entity from which query results are obtained.
* @param expression an expression that computes a value by which to
* order results. Alternatively, {@code null} if
* {@code property} is supplied instead.
* @param property name of an entity attribute to order by.
* Alternatively, {@code null} if {@code expression}
* is supplied instead.
* @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
Expand All @@ -88,7 +96,8 @@
* {@link Nulls#FIRST FIRST}, {@link Nulls#LAST LAST}, or
* {@linkplain Nulls#UNSPECIFIED by the data store}.
*/
public record Sort<T>(String property,
public record Sort<T>(ComparableExpression<T, ? extends Comparable<?>> expression,
String property,
boolean isAscending,
boolean ignoreCase,
Nulls nullOrdering) {
Expand Down Expand Up @@ -159,15 +168,21 @@ public enum Nulls {
* @since 1.1
*/
public Sort {
if (property == null) {
if (expression == null && property == null) {
throw new NullPointerException(
Messages.get("001.arg.required", "attribute"));
Messages.get("001.arg.required", "expression"));
}

if (nullOrdering == null) {
throw new NullPointerException(
Messages.get("001.arg.required", "nullOrdering"));
}

if (property != null && expression != null) {
// TODO add a message
throw new IllegalArgumentException(
"property: " + property + ", expression: " + expression);
}
}

/**
Expand All @@ -181,18 +196,39 @@ public enum Nulls {
* from a database with case sensitive collation.
*/
public Sort(String property, boolean isAscending, boolean ignoreCase) {
this(property, isAscending, ignoreCase, Nulls.UNSPECIFIED);
this(null,
property,
isAscending,
ignoreCase,
Nulls.UNSPECIFIED);
}

// Override to provide method documentation:

/**
* Name of the entity attribute to order by.
* An expression to order by. The presence of a sort expression is
* mutually exclusive with the presence of a
* {@linkplain #property() sort attribute name}.
*
* @return The attribute name to order by; will never be {@code null}.
* @return The attribute name to order by, or {@code null} if this
* {@code Sort} instance pertains to a {@link #property()}
*/
public ComparableExpression<T, ? extends Comparable<?>> expression() {
return expression;
}

/**
* Name of the entity attribute to order by The presence of a
* sort attribute name is mutually exclusive with the presence of a
* sort {@link #expression()}.
*
* @return The attribute name to order by, or {@code null} if this
* {@code Sort} instance pertains to an {@link #expression()}
*/
public String property() {
return property;
return expression instanceof Attribute<?> attr
? attr.name()
: property;
}

// Override to provide method documentation:
Expand Down Expand Up @@ -262,7 +298,8 @@ public static <T> Sort<T> of(String attribute,
Messages.get("001.arg.required", "direction"));
}

return new Sort<>(attribute,
return new Sort<>(null,
attribute,
Direction.ASC.equals(direction),
ignoreCase,
Nulls.UNSPECIFIED);
Expand Down Expand Up @@ -292,7 +329,8 @@ public static <T> Sort<T> of(String attribute,
Messages.get("001.arg.required", "direction"));
}

return new Sort<>(attribute,
return new Sort<>(null,
attribute,
Direction.ASC.equals(direction),
ignoreCase,
nullOrdering);
Expand All @@ -309,7 +347,7 @@ public static <T> Sort<T> of(String attribute,
* @throws NullPointerException when the attribute name is null.
*/
public static <T> Sort<T> asc(String attribute) {
return new Sort<>(attribute, true, false, Nulls.UNSPECIFIED);
return new Sort<>(null, attribute, true, false, Nulls.UNSPECIFIED);
}

/**
Expand All @@ -322,7 +360,7 @@ public static <T> Sort<T> asc(String attribute) {
* @throws NullPointerException when the attribute name is null.
*/
public static <T> Sort<T> ascIgnoreCase(String attribute) {
return new Sort<>(attribute, true, true, Nulls.UNSPECIFIED);
return new Sort<>(null, attribute, true, true, Nulls.UNSPECIFIED);
}

/**
Expand All @@ -336,7 +374,7 @@ public static <T> Sort<T> ascIgnoreCase(String attribute) {
* @throws NullPointerException when the attribute name is null.
*/
public static <T> Sort<T> desc(String attribute) {
return new Sort<>(attribute, false, false, Nulls.UNSPECIFIED);
return new Sort<>(null, attribute, false, false, Nulls.UNSPECIFIED);
}

/**
Expand All @@ -350,7 +388,7 @@ public static <T> Sort<T> desc(String attribute) {
* @throws NullPointerException when the attribute name is null.
*/
public static <T> Sort<T> descIgnoreCase(String attribute) {
return new Sort<>(attribute, false, true, Nulls.UNSPECIFIED);
return new Sort<>(null, attribute, false, true, Nulls.UNSPECIFIED);
}

/**
Expand All @@ -374,7 +412,11 @@ public static <T> Sort<T> descIgnoreCase(String attribute) {
* @since 1.1
*/
public Sort<T> nullsFirst() {
return new Sort<>(property, isAscending, ignoreCase, Nulls.FIRST);
return new Sort<>(expression,
property,
isAscending,
ignoreCase,
Nulls.FIRST);
}

/**
Expand All @@ -398,6 +440,10 @@ public Sort<T> nullsFirst() {
* @since 1.1
*/
public Sort<T> nullsLast() {
return new Sort<>(property, isAscending, ignoreCase, Nulls.LAST);
return new Sort<>(expression,
property,
isAscending,
ignoreCase,
Nulls.LAST);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import jakarta.data.constraint.Between;
import jakarta.data.constraint.GreaterThan;
import jakarta.data.Sort;
import jakarta.data.Sort.Nulls;
import jakarta.data.constraint.AtLeast;
import jakarta.data.constraint.LessThan;
import jakarta.data.constraint.AtMost;
Expand Down Expand Up @@ -46,6 +48,16 @@
public interface ComparableExpression<T, V extends Comparable<?>>
extends Expression<T, V> {

/**
* Obtain a request for an ascending {@link Sort} based on the value
* to which this expression computes.
*
* @return a request for an ascending sort.
*/
default Sort<T> asc() {
return new Sort<T>(this, null, true, false, Nulls.UNSPECIFIED);
}

/**
* <p>Obtains a {@link Restriction} that requires that this expression
* evaluate to a value falling within the range between (and inclusive of)
Expand Down Expand Up @@ -95,6 +107,16 @@ default <U extends ComparableExpression<? super T, V>> Restriction<T> between(
Between.bounds(minExpression, maxExpression));
}

/**
* Obtain a request for a descending {@link Sort} based on the value
* to which this expression computes.
*
* @return a request for a descending sort.
*/
default Sort<T> desc() {
return new Sort<>(this, null, false, false, Nulls.UNSPECIFIED);
}

/**
* <p>Obtains a {@link Restriction} that requires that this expression
* evaluate to a value greater than the given value.</p>
Expand Down
22 changes: 22 additions & 0 deletions api/src/main/java/jakarta/data/expression/TextExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import static jakarta.data.spi.expression.function.TextFunctionExpression.RIGHT;
import static jakarta.data.spi.expression.function.TextFunctionExpression.UPPER;

import jakarta.data.Sort;
import jakarta.data.Sort.Nulls;
import jakarta.data.constraint.Like;
import jakarta.data.constraint.NotLike;
import jakarta.data.messages.Messages;
Expand All @@ -46,6 +48,26 @@
*/
public interface TextExpression<T> extends ComparableExpression<T, String> {

/**
* Obtain a request for an ascending, case-insensitive {@link Sort}
* based on the value to which this expression computes.
*
* @return a request for an ascending, case-insensitive sort.
*/
default Sort<T> ascIgnoreCase() {
return new Sort<>(this, null, true, true, Nulls.UNSPECIFIED);
}

/**
* Obtain a request for a descending, case insensitive {@link Sort}
* based on the value to which this expression computes.
*
* @return a request for a descending, case insensitive sort.
*/
default Sort<T> descIgnoreCase() {
return new Sort<>(this, null, false, true, Nulls.UNSPECIFIED);
}

/**
* Returns {@code String.class} as the type of the textual expression.
*
Expand Down
23 changes: 23 additions & 0 deletions api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package jakarta.data.metamodel;

import jakarta.data.Sort;
import jakarta.data.expression.ComparableExpression;
import jakarta.data.messages.Messages;

Expand Down Expand Up @@ -86,5 +87,27 @@ static <T, V extends Comparable<?>> ComparableAttribute<T, V> of(

return new ComparableAttributeRecord<>(entityClass, name, attributeType);
}

/**
* Obtain a request for an ascending {@link Sort} based on the entity
* attribute.
*
* @return a request for an ascending sort on the entity attribute.
*/
@Override
default Sort<T> 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.
*/
@Override
default Sort<T> desc() {
return Sort.desc(name());
}
}

4 changes: 3 additions & 1 deletion api/src/main/java/jakarta/data/metamodel/TextAttribute.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023,2025 Contributors to the Eclipse Foundation
* Copyright (c) 2023,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.
Expand Down Expand Up @@ -35,6 +35,7 @@ public interface TextAttribute<T> extends ComparableAttribute<T, String>, TextEx
* @return a request for an ascending, case-insensitive sort on the entity
* attribute.
*/
@Override
default Sort<T> ascIgnoreCase() {
return Sort.ascIgnoreCase(name());
}
Expand All @@ -58,6 +59,7 @@ default Class<String> type() {
* @return a request for a descending, case insensitive sort on the entity
* attribute.
*/
@Override
default Sort<T> descIgnoreCase() {
return Sort.descIgnoreCase(name());
}
Expand Down
50 changes: 50 additions & 0 deletions api/src/test/java/jakarta/data/expression/ExpressionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;

import jakarta.data.Sort;
import jakarta.data.constraint.EqualTo;
import jakarta.data.constraint.AtMost;
import jakarta.data.mock.entity.Book;
Expand Down Expand Up @@ -160,4 +161,53 @@ void shouldRestrictLengthOfText() {
.isEqualTo(50);
});
}

@Test
void testSortOnLeft3() {
TextExpression<Book> first3LettersOfTitle = _Book.title.right(3);
Sort<Book> first3LettersOfTitleAsc =
first3LettersOfTitle.ascIgnoreCase();

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(first3LettersOfTitleAsc.expression())
.isEqualTo(first3LettersOfTitle);

soft.assertThat(first3LettersOfTitleAsc.isAscending())
.isTrue();

soft.assertThat(first3LettersOfTitleAsc.isDescending())
.isFalse();

soft.assertThat(first3LettersOfTitleAsc.ignoreCase())
.isEqualTo(true);

soft.assertThat(first3LettersOfTitleAsc.property())
.isNull();
});
}

@Test
void testSortOnDivisionExpression() {
NumericExpression<Book, Integer> pagesPerChapter =
_Book.numPages.dividedBy(_Book.numChapters);
Sort<Book> pagesPerChapterDesc = pagesPerChapter.desc();

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(pagesPerChapterDesc.expression())
.isEqualTo(pagesPerChapter);

soft.assertThat(pagesPerChapterDesc.isAscending())
.isFalse();

soft.assertThat(pagesPerChapterDesc.isDescending())
.isTrue();

soft.assertThat(pagesPerChapterDesc.ignoreCase())
.isEqualTo(false);

soft.assertThat(pagesPerChapterDesc.property())
.isNull();
});
}

}
Loading