Repository: jakartaee/data
Pull request #27 (2022-09-29 05:00:29)

Programmatically specify sorting of results

* This PR solves: https://github.com/jakartaee/data/issues/21
* This PR might solve: https://github.com/jakartaee/data/issues/23

:warning: This PR won't cover pagination support; it will only talk about unique parameters to solve sorting, limit, and skip.

:bomb: It is still missing the default implementation and its respective tests; I'll cover them once we agree with the interfaces.


Patch:
diff --git a/api/pom.xml b/api/pom.xml
index 32e225ac..5665d1c5 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -17,7 +17,8 @@
   ~ SPDX-License-Identifier: Apache-2.0
   -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <parent>
@@ -31,6 +32,12 @@
     <description>Jakarta Data :: API</description>
 
     <dependencies>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-all</artifactId>
+            <version>1.3</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/api/src/main/java/jakarta/data/DataException.java b/api/src/main/java/jakarta/data/DataException.java
new file mode 100644
index 00000000..a8382731
--- /dev/null
+++ b/api/src/main/java/jakarta/data/DataException.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 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;
+
+/**
+ * Thrown by the data provider when a problem occurs.
+ */
+public class DataException extends RuntimeException {
+
+
+    /**
+     * Constructs a new DataException exception with the specified detail message.
+     *
+     * @param message the detail message.
+     */
+    public DataException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new DataException exception with the specified detail message and cause.
+     *
+     * @param message the detail message.
+     * @param cause   the cause.
+     */
+    public DataException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new DataException exception with the specified cause.
+     *
+     * @param cause the cause.
+     */
+    public DataException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/api/src/main/java/jakarta/data/repository/Direction.java b/api/src/main/java/jakarta/data/repository/Direction.java
new file mode 100644
index 00000000..c47fb670
--- /dev/null
+++ b/api/src/main/java/jakarta/data/repository/Direction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022 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.repository;
+
+/**
+ * Represents the direction of the ordering.
+ */
+public enum Direction {
+    /**
+     * Ascending direction
+     */
+    ASC,
+    /**
+     * Descending direction
+     */
+    DESC;
+}
diff --git a/api/src/main/java/jakarta/data/repository/Pageable.java b/api/src/main/java/jakarta/data/repository/Pageable.java
new file mode 100644
index 00000000..fe42ea55
--- /dev/null
+++ b/api/src/main/java/jakarta/data/repository/Pageable.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2022 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.repository;
+
+import java.util.Objects;
+
+/**
+ * Abstract interface for pagination information.
+ */
+public class Pageable {
+
+    private static final long DEFAULT_SIZE = 10;
+
+    private final long size;
+
+    private final long page;
+
+    private Pageable(long size, long page) {
+        this.size = size;
+        this.page = page;
+    }
+
+    /**
+     * Returns the size of each page
+     *
+     * @return the size of each page
+     */
+    public long getSize() {
+        return size;
+    }
+
+    /**
+     * Returns the page to be returned.
+     *
+     * @return the page to be returned.
+     */
+    public long getPage() {
+        return page;
+    }
+
+    /**
+     * Returns the Pageable requesting the next Page.
+     *
+     * @return The next pageable.
+     */
+    public Pageable next() {
+        return new Pageable(this.size, (page + 1));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Pageable pageable = (Pageable) o;
+        return size == pageable.size && page == pageable.page;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(size, page);
+    }
+
+    @Override
+    public String toString() {
+        return "Pageable{" +
+                "size=" + size +
+                ", page=" + page +
+                '}';
+    }
+
+    /**
+     * Creates a new Pageable at the given size with a default size of 10.
+     *
+     * @param page The page
+     * @return The pageable
+     */
+    public static Pageable page(long page) {
+        return of(page, DEFAULT_SIZE);
+    }
+
+    /**
+     * Creates a new Pageable at the given size and page
+     *
+     * @param page The page
+     * @param size The size
+     * @return The pageable
+     */
+    public static Pageable of(long page, long size) {
+        if (page < 1) {
+            throw new IllegalArgumentException("page: " + page);
+        } else if (size < 1) {
+            throw new IllegalArgumentException("size: " + size);
+        }
+        return new Pageable(size, page);
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/repository/Sort.java b/api/src/main/java/jakarta/data/repository/Sort.java
new file mode 100644
index 00000000..1307d047
--- /dev/null
+++ b/api/src/main/java/jakarta/data/repository/Sort.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2022 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.repository;
+
+import java.util.Objects;
+
+/**
+ * Order implements the pairing of a {@link Direction} and a property. It is used to provide input for Sort
+ */
+public class Sort {
+
+    private final String property;
+
+    private final Direction direction;
+
+    private Sort(String property, Direction direction) {
+        this.property = property;
+        this.direction = direction;
+    }
+
+    /**
+     * @return The property name to order by
+     */
+    public String getProperty() {
+        return this.property;
+    }
+
+    /**
+     * @return Returns whether sorting for this property shall be ascending.
+     */
+    public boolean isAscending() {
+        return Direction.ASC.equals(direction);
+    }
+
+    /**
+     * @return Returns whether sorting for this property shall be descending.
+     */
+    public boolean isDescending() {
+        return Direction.DESC.equals(direction);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Sort sort = (Sort) o;
+        return Objects.equals(property, sort.property) && direction == sort.direction;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(property, direction);
+    }
+
+    @Override
+    public String toString() {
+        return "Sort{" +
+                "property='" + property + '\'' +
+                ", direction=" + direction +
+                '}';
+    }
+
+    /**
+     * Create a {@link Sort} instance
+     *
+     * @param property  the property name to order by
+     * @param direction The direction order by
+     * @return an {@link Sort} instance
+     * @throws NullPointerException when there is a null parameter
+     */
+    public static Sort of(String property, Direction direction) {
+        Objects.requireNonNull(property, "property is required");
+        Objects.requireNonNull(direction, "direction is required");
+        return new Sort(property, direction);
+    }
+
+    /**
+     * Create a {@link Sort} instance with ascending direction {@link  Direction#ASC}
+     *
+     * @param property the property name to order by
+     * @return the Order type
+     * @return an {@link Sort} instance
+     * @throws NullPointerException when the property is null
+     */
+    public static Sort asc(String property) {
+        return of(property, Direction.ASC);
+    }
+
+    /**
+     * Create a {@link Sort} instance on descending direction {@link  Direction#DESC}
+     *
+     * @param property the property name to order by
+     * @return the Order type
+     * @return an {@link Sort} instance
+     * @throws NullPointerException when the property is null
+     */
+    public static Sort desc(String property) {
+        return of(property, Direction.DESC);
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java
index 595f5892..d65b5809 100644
--- a/api/src/main/java/module-info.java
+++ b/api/src/main/java/module-info.java
@@ -18,4 +18,5 @@
 module jakarta.data.api {
     exports jakarta.data;
     exports jakarta.data.repository;
+    opens jakarta.data.repository;
 }
\ No newline at end of file
diff --git a/spec/src/main/asciidoc/repository.asciidoc b/spec/src/main/asciidoc/repository.asciidoc
index 4f653122..b3bc7742 100644
--- a/spec/src/main/asciidoc/repository.asciidoc
+++ b/spec/src/main/asciidoc/repository.asciidoc
@@ -219,3 +219,37 @@ Jakarta Data implementations often support the following list of predicate keywo
 |findByNameOrderByAgeAscNameDescYearAsc
 
 |===
+
+===== Special parameter handling
+
+Jakarta Data also supports particular parameters to define pagination and sorting.
+
+Jakarta Data recognizes, when specified on a repository method after the query parameters, specific types, like ```Pageable``` and ```Sort```, to dynamically apply pagination and sorting to queries. The following example demonstrates these features:
+
+[source,java]
+----
+@Repository
+public interface ProductRepository extends CrudRepository<Product, Long> {
+
+  List<Product> findByName(String name, Pageable pageable);
+
+  List<Product> findByName(String name, Sort sort);
+
+}
+----
+
+You can define simple sorting expressions by using property names.
+
+[source,java]
+----
+Sort name = Sort.asc("name");
+----
+
+You can combine sorting with start and size by using property names.
+
+[source,java]
+----
+Pageable pageable = Pageable.of(1, 20);
+first20 = products.findByNameLike(name, pageable, Sort.desc("price"));
+
+----
\ No newline at end of file


Test patch:
diff --git a/api/src/test/java/jakarta/data/repository/PageableTest.java b/api/src/test/java/jakarta/data/repository/PageableTest.java
new file mode 100644
index 00000000..6424302e
--- /dev/null
+++ b/api/src/test/java/jakarta/data/repository/PageableTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2022 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.repository;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class PageableTest {
+
+    @Test
+    public void shouldCreatePageable(){
+        Pageable pageable = Pageable.of(2, 6);
+        Assertions.assertEquals(6L, pageable.getSize());
+        Assertions.assertEquals(2L, pageable.getPage());
+    }
+
+    @Test
+    public void shouldNext(){
+        Pageable pageable = Pageable.of(2, 1);
+        Pageable next = pageable.next();
+        Assertions.assertEquals(1L, pageable.getSize());
+        Assertions.assertEquals(2L, pageable.getPage());
+        Assertions.assertEquals(3L, next.getPage());
+        Assertions.assertEquals(1L, next.getSize());
+    }
+
+    @Test
+    public void shouldReturnErrorWhenThereIsIllegalArgument() {
+        Assertions.assertThrows(IllegalArgumentException.class, ()->
+                Pageable.page(0));
+
+        Assertions.assertThrows(IllegalArgumentException.class, ()->
+                Pageable.page(-1));
+
+        Assertions.assertThrows(IllegalArgumentException.class, ()->
+                Pageable.of(1, -1));
+
+        Assertions.assertThrows(IllegalArgumentException.class, ()->
+                Pageable.of(1, 0));
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/test/java/jakarta/data/repository/SortTest.java b/api/src/test/java/jakarta/data/repository/SortTest.java
new file mode 100644
index 00000000..9324537a
--- /dev/null
+++ b/api/src/test/java/jakarta/data/repository/SortTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2022 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.repository;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class SortTest {
+
+    @Test
+    public void shouldReturnErrorWhenPropertyDirectionNull() {
+        Assertions.assertThrows(NullPointerException.class, () ->
+                Sort.of(null, null));
+
+        Assertions.assertThrows(NullPointerException.class, () ->
+                Sort.of("name", null));
+
+        Assertions.assertThrows(NullPointerException.class, () ->
+                Sort.of(null, Direction.ASC));
+    }
+
+    @Test
+    public void shouldCreateSort() {
+        Sort order = Sort.of("name", Direction.ASC);
+        Assertions.assertNotNull(order);
+        Assertions.assertEquals("name", order.getProperty());
+        Assertions.assertTrue(order.isAscending());
+        Assertions.assertFalse(order.isDescending());
+    }
+
+    @Test
+    public void shouldCreateAsc(){
+        Sort order = Sort.asc("name");
+        Assertions.assertNotNull(order);
+        Assertions.assertEquals("name", order.getProperty());
+        Assertions.assertTrue(order.isAscending());
+        Assertions.assertFalse(order.isDescending());
+    }
+
+    @Test
+    public void shouldCreateDesc(){
+        Sort order = Sort.desc("name");
+        Assertions.assertNotNull(order);
+        Assertions.assertEquals("name", order.getProperty());
+        Assertions.assertTrue(order.isDescending());
+        Assertions.assertFalse(order.isAscending());
+    }
+
+    @Test
+    public void shouldCreateOrder() {
+        Sort order = Sort.of("name",Direction.ASC);
+        Assertions.assertNotNull(order);
+        Assertions.assertEquals("name", order.getProperty());
+    }
+
+    @Test
+    public void shouldAscending(){
+        Sort order = Sort.of("name",Direction.ASC);
+        Assertions.assertEquals("name", order.getProperty());
+        Assertions.assertTrue(order.isAscending());
+        Assertions.assertFalse(order.isDescending());
+    }
+
+    @Test
+    public void shouldDescending(){
+        Sort order = Sort.of("name",Direction.DESC);
+        Assertions.assertEquals("name", order.getProperty());
+        Assertions.assertFalse(order.isAscending());
+        Assertions.assertTrue(order.isDescending());
+    }
+}
\ No newline at end of file