(this, null, true, false, Nulls.UNSPECIFIED);
+ }
+
/**
* Obtains a {@link Restriction} that requires that this expression
* evaluate to a value falling within the range between (and inclusive of)
@@ -95,6 +107,16 @@ default > Restriction 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 desc() {
+ return new Sort<>(this, null, false, false, Nulls.UNSPECIFIED);
+ }
+
/**
* Obtains a {@link Restriction} that requires that this expression
* evaluate to a value greater than the given value.
diff --git a/api/src/main/java/jakarta/data/expression/TextExpression.java b/api/src/main/java/jakarta/data/expression/TextExpression.java
index 2b813f1a1..d5d9c37a3 100644
--- a/api/src/main/java/jakarta/data/expression/TextExpression.java
+++ b/api/src/main/java/jakarta/data/expression/TextExpression.java
@@ -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;
@@ -46,6 +48,26 @@
*/
public interface TextExpression extends ComparableExpression {
+ /**
+ * 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 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 descIgnoreCase() {
+ return new Sort<>(this, null, false, true, Nulls.UNSPECIFIED);
+ }
+
/**
* Returns {@code String.class} as the type of the textual expression.
*
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
index 8440333c0..d476cc734 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
@@ -17,6 +17,7 @@
*/
package jakarta.data.metamodel;
+import jakarta.data.Sort;
import jakarta.data.expression.ComparableExpression;
import jakarta.data.messages.Messages;
@@ -86,5 +87,27 @@ static > ComparableAttribute 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 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 desc() {
+ return Sort.desc(name());
+ }
}
diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
index 0eeb7c4aa..0c51a1976 100644
--- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
@@ -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.
@@ -35,6 +35,7 @@ public interface TextAttribute extends ComparableAttribute, TextEx
* @return a request for an ascending, case-insensitive sort on the entity
* attribute.
*/
+ @Override
default Sort ascIgnoreCase() {
return Sort.ascIgnoreCase(name());
}
@@ -58,6 +59,7 @@ default Class type() {
* @return a request for a descending, case insensitive sort on the entity
* attribute.
*/
+ @Override
default Sort descIgnoreCase() {
return Sort.descIgnoreCase(name());
}
diff --git a/api/src/test/java/jakarta/data/expression/ExpressionTest.java b/api/src/test/java/jakarta/data/expression/ExpressionTest.java
index 0778aa38b..c0abca1fe 100644
--- a/api/src/test/java/jakarta/data/expression/ExpressionTest.java
+++ b/api/src/test/java/jakarta/data/expression/ExpressionTest.java
@@ -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;
@@ -160,4 +161,53 @@ void shouldRestrictLengthOfText() {
.isEqualTo(50);
});
}
+
+ @Test
+ void testSortOnLeft3() {
+ TextExpression first3LettersOfTitle = _Book.title.right(3);
+ Sort 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 pagesPerChapter =
+ _Book.numPages.dividedBy(_Book.numChapters);
+ Sort 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();
+ });
+ }
+
}
diff --git a/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java b/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java
index 209c91800..1585bf173 100644
--- a/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java
+++ b/api/src/test/java/jakarta/data/spi/expression/path/ComparablePathTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2025,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.
@@ -21,7 +21,6 @@
import jakarta.data.expression.NavigableExpression;
import jakarta.data.metamodel.ComparableAttribute;
import jakarta.data.metamodel.NavigableAttribute;
-import jakarta.data.spi.expression.path.ComparablePath;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.DisplayName;
@@ -56,7 +55,7 @@ void shouldCreateComparablePath() {
NavigableExpression expr = _Author.publisher;
ComparableAttribute attr = _Publisher.rating;
- var path = ComparablePath.of(expr, attr);
+ var path = expr.navigate(attr);
SoftAssertions.assertSoftly(soft -> {
soft.assertThat(path).isNotNull();
diff --git a/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java b/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
index 52d0d1561..629a05795 100644
--- a/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
+++ b/api/src/test/java/jakarta/data/spi/expression/path/NavigablePathTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * Copyright (c) 2025,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.
@@ -17,9 +17,20 @@
*/
package jakarta.data.spi.expression.path;
+import java.time.LocalDate;
+
+import jakarta.data.expression.BooleanExpression;
+import jakarta.data.expression.ComparableExpression;
import jakarta.data.expression.NavigableExpression;
+import jakarta.data.expression.NumericExpression;
+import jakarta.data.expression.TemporalExpression;
+import jakarta.data.expression.TextExpression;
+import jakarta.data.metamodel.BooleanAttribute;
+import jakarta.data.metamodel.ComparableAttribute;
import jakarta.data.metamodel.NavigableAttribute;
-import jakarta.data.spi.expression.path.NavigablePath;
+import jakarta.data.metamodel.NumericAttribute;
+import jakarta.data.metamodel.TemporalAttribute;
+import jakarta.data.metamodel.TextAttribute;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.DisplayName;
@@ -30,17 +41,48 @@
*/
class NavigablePathTest {
- static class Publisher {
+ static enum BusinessType {
+ COMMERCIAL,
+ GOVERNMENT,
+ NONPROFIT
+ }
+
+ static class BusinessInfo {
+ Boolean active;
+ LocalDate founded;
String name;
+ BusinessType type;
+ int zipcode;
+ }
+
+ static class Publisher {
+ BusinessInfo info;
}
static class Book {
Publisher publisher;
}
+ interface _BusinessInfo {
+ BooleanAttribute active =
+ BooleanAttribute.of(BusinessInfo.class, "active", Boolean.class);
+
+ TemporalAttribute founded =
+ TemporalAttribute.of(BusinessInfo.class, "founded", LocalDate.class);
+
+ TextAttribute name =
+ TextAttribute.of(BusinessInfo.class, "name");
+
+ ComparableAttribute type =
+ ComparableAttribute.of(BusinessInfo.class, "type", BusinessType.class);
+
+ NumericAttribute zipcode =
+ NumericAttribute.of(BusinessInfo.class, "zipcode", int.class);
+ }
+
interface _Publisher {
- NavigableAttribute name =
- NavigableAttribute.of(Publisher.class, "name", String.class);
+ NavigableAttribute info =
+ NavigableAttribute.of(Publisher.class, "info", BusinessInfo.class);
}
interface _Book {
@@ -49,18 +91,115 @@ interface _Book {
}
@Test
- @DisplayName("should create NavigablePath from expression and attribute")
+ @DisplayName("should create BooleanPath from path and attribute")
+ void shouldCreateBooleanPath() {
+ BooleanExpression publisherInfoActive =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.active);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfoActive.asc().expression().toString())
+ .isEqualTo("book.publisher.info.active");
+ soft.assertThat(publisherInfoActive.desc().expression().toString())
+ .isEqualTo("book.publisher.info.active");
+ soft.assertThat(publisherInfoActive.toString())
+ .isEqualTo("book.publisher.info.active");
+ });
+ }
+
+ @Test
+ @DisplayName("should create ComparablePath from path and attribute")
+ void shouldCreateComparablePath() {
+ ComparableExpression publisherBusinessInfoType =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.type);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherBusinessInfoType.asc().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.desc().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.type");
+ soft.assertThat(publisherBusinessInfoType.toString())
+ .isEqualTo("book.publisher.info.type");
+ });
+ }
+
+ @Test
+ @DisplayName("should create NavigablePath from path and attribute")
void shouldCreateNavigablePath() {
- NavigableExpression expr = _Book.publisher;
- NavigableAttribute attr = _Publisher.name;
- var path = NavigablePath.of(expr, attr);
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(_Book.publisher.name())
+ .isEqualTo("publisher");
+ soft.assertThat(_Book.publisher.toString())
+ .isEqualTo("book.publisher");
+ });
+
+ NavigableExpression publisherInfo =
+ _Book.publisher.navigate(_Publisher.info);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfo.toString())
+ .isEqualTo("book.publisher.info");
+ });
+ }
+
+ @Test
+ @DisplayName("should create NumericPath from path and attribute")
+ void shouldCreateNumericPath() {
+ NumericExpression publisherInfoZipcode =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.zipcode);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfoZipcode.asc().expression().toString())
+ .isEqualTo("book.publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.desc().expression().toString())
+ .isEqualTo("book.publisher.info.zipcode");
+ soft.assertThat(publisherInfoZipcode.toString())
+ .isEqualTo("book.publisher.info.zipcode");
+ });
+ }
+
+ @Test
+ @DisplayName("should create TemporalPath from path and attribute")
+ void shouldCreateTemporalPath() {
+ TemporalExpression publisherInfoFounded =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.founded);
+
+ SoftAssertions.assertSoftly(soft -> {
+ soft.assertThat(publisherInfoFounded.asc().expression().toString())
+ .isEqualTo("book.publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.desc().expression().toString())
+ .isEqualTo("book.publisher.info.founded");
+ soft.assertThat(publisherInfoFounded.toString())
+ .isEqualTo("book.publisher.info.founded");
+ });
+ }
+
+ @Test
+ @DisplayName("should create TextPath from path and attribute")
+ void shouldCreateTextPath() {
+ TextExpression publisherInfoName =
+ _Book.publisher.navigate(_Publisher.info)
+ .navigate(_BusinessInfo.name);
SoftAssertions.assertSoftly(soft -> {
- soft.assertThat(path).isNotNull();
- soft.assertThat(path).isInstanceOf(NavigableExpression.class);
- soft.assertThat(path.toString())
- .isEqualTo("book.publisher.name");
+ soft.assertThat(publisherInfoName.asc().expression().toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.desc().expression().toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.ascIgnoreCase().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.descIgnoreCase().expression()
+ .toString())
+ .isEqualTo("book.publisher.info.name");
+ soft.assertThat(publisherInfoName.toString())
+ .isEqualTo("book.publisher.info.name");
});
}
}