diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 52a87a02356fc..d6809b2101257 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -172,7 +172,7 @@
0.3.0
4.14.0
6.1.SP2
- 3.2.SP1
+ 3.2.SP2
6.2
3.2
5.12.0
diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java
index 099b21998494b..22b936f891448 100644
--- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java
+++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java
@@ -53,10 +53,6 @@ public class RepositoryMethodsImplementor implements ResourceMethodsImplementor
public static final MethodDescriptor LIST_PAGED = ofMethod(PagingAndSortingRepository.class, "findAll",
org.springframework.data.domain.Page.class, Pageable.class);
- //ListPagingAndSortingRepository
- public static final MethodDescriptor LIST_SORTED = ofMethod(ListPagingAndSortingRepository.class, "findAll",
- List.class, org.springframework.data.domain.Sort.class);
-
private static final Class> PANACHE_PAGE = io.quarkus.panache.common.Page.class;
private static final Class> PANACHE_SORT = io.quarkus.panache.common.Sort.class;
@@ -83,7 +79,7 @@ public RepositoryMethodsImplementor(IndexView index, EntityClassHelper entityCla
}
// CrudRepository Iterable findAll();
- public void implementListIterable(ClassCreator classCreator, String repositoryInterfaceName) {
+ public void implementIterable(ClassCreator classCreator, String repositoryInterfaceName) {
if (entityClassHelper.isCrudRepository(repositoryInterfaceName)
&& !entityClassHelper.isPagingAndSortingRepository(repositoryInterfaceName)) {
MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class,
@@ -91,26 +87,29 @@ public void implementListIterable(ClassCreator classCreator, String repositoryIn
ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName);
ResultHandle result = methodCreator.invokeInterfaceMethod(LIST_ITERABLE, repository);
methodCreator.returnValue(result);
- LOGGER.infof("Method code: %s ", methodCreator.getMethodDescriptor().toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.getMethodDescriptor().toString());
methodCreator.close();
}
}
//ListCrudRepository List findAll();
public void implementList(ClassCreator classCreator, String repositoryInterfaceName) {
- if (entityClassHelper.isListCrudRepository(repositoryInterfaceName)) {
+ if (entityClassHelper.isListCrudRepository(repositoryInterfaceName)
+ && !entityClassHelper.isListPagingAndSortingRepository(repositoryInterfaceName)) {
MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class,
String.class, Map.class);
ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName);
ResultHandle result = methodCreator.invokeInterfaceMethod(LIST, repository);
methodCreator.returnValue(result);
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
}
// PagingAndSortingRepository Page findAll(Pageable pageable);
- public void implementListPaged(ClassCreator classCreator, String repositoryInterfaceName) {
+ // PagingAndSortingRepository Iterable findAll(Pageable pageable);
+ // ListPagingAndSortingRepository List findAll(Sort sort);
+ public void implementPagedList(ClassCreator classCreator, String repositoryInterfaceName) {
if (entityClassHelper.isPagingAndSortingRepository(repositoryInterfaceName)) {
MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class,
io.quarkus.panache.common.Sort.class, String.class, Map.class);
@@ -124,26 +123,7 @@ public void implementListPaged(ClassCreator classCreator, String repositoryInter
ofMethod(org.springframework.data.domain.Page.class, "getContent", List.class), resultPage);
methodCreator.returnValue(result);
- LOGGER.infof("Method code: %s ", methodCreator.toString());
- methodCreator.close();
- }
- }
-
- //ListPagingAndSortingRepository List findAll(Sort sort);
- public void implementListSort(ClassCreator classCreator, String repositoryInterfaceName) {
- if (entityClassHelper.isListPagingAndSortingRepository(repositoryInterfaceName)) {
- MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class,
- io.quarkus.panache.common.Sort.class, String.class, Map.class);
- ResultHandle page = methodCreator.getMethodParam(0);
- ResultHandle sort = methodCreator.getMethodParam(1);
- ResultHandle pageable = toPageable(methodCreator, page, sort);
- ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName);
- ResultHandle resultPage = methodCreator.invokeInterfaceMethod(LIST_SORTED, repository, pageable);
- ResultHandle result = methodCreator.invokeInterfaceMethod(
- ofMethod(org.springframework.data.domain.Page.class, "getContent", List.class), resultPage);
-
- methodCreator.returnValue(result);
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
}
@@ -163,7 +143,7 @@ public void implementListPageCount(ClassCreator classCreator, String repositoryI
} else {
methodCreator.throwException(RuntimeException.class, "Method not implemented");
}
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
@@ -175,7 +155,7 @@ public void implementListById(ClassCreator classCreator, String repositoryInterf
ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName);
ResultHandle result = methodCreator.invokeInterfaceMethod(LIST_BY_ID, repository, ids);
methodCreator.returnValue(result);
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
}
@@ -191,7 +171,7 @@ public void implementGet(ClassCreator classCreator, String repositoryInterfaceNa
} else {
methodCreator.throwException(RuntimeException.class, "Method not implemented");
}
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
@@ -208,7 +188,7 @@ public void implementAdd(ClassCreator classCreator, String repositoryInterfaceNa
methodCreator.throwException(RuntimeException.class, "Method not implemented");
}
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
@@ -223,7 +203,7 @@ public void implementAddList(ClassCreator classCreator, String repositoryInterfa
} else {
methodCreator.throwException(RuntimeException.class, "Method not implemented");
}
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
@@ -240,7 +220,7 @@ public void implementUpdate(ClassCreator classCreator, String repositoryInterfac
} else {
methodCreator.throwException(RuntimeException.class, "Method not implemented");
}
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
@@ -261,7 +241,7 @@ public void implementDelete(ClassCreator classCreator, String repositoryInterfac
} else {
methodCreator.throwException(RuntimeException.class, "Method not implemented");
}
- LOGGER.infof("Method code: %s ", methodCreator.toString());
+ LOGGER.debugf("Method code: %s ", methodCreator.toString());
methodCreator.close();
}
diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java
index 68e630c90ac4c..4bae869b393b6 100644
--- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java
+++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java
@@ -6,7 +6,6 @@
import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST;
import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST_ITERABLE;
import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST_PAGED;
-import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST_SORTED;
import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.SAVE_LIST;
import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.UPDATE;
@@ -34,8 +33,6 @@ protected Map> getMethodPredicates() {
methodPredicates.put("listPaged", methodInfo -> methodInfo.name().equals(LIST_PAGED.getName())
&& methodInfo.parametersCount() == 1
&& methodInfo.parameterType(0).name().equals(PAGEABLE));
- methodPredicates.put("listSorted",
- methodInfo -> methodInfo.name().equals(LIST_SORTED.getName()) && methodInfo.parameterTypes().isEmpty());
methodPredicates.put("addAll", methodInfo -> methodInfo.name().equals(SAVE_LIST.getName()));
methodPredicates.put("get", methodInfo -> methodInfo.name().equals(GET.getName()));
methodPredicates.put("add", methodInfo -> methodInfo.name().equals(ADD.getName()));
diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java
index 4f0f503378822..1679853f15c81 100644
--- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java
+++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java
@@ -33,10 +33,9 @@ public String implement(ClassOutput classOutput, String resourceType, String ent
.build();
classCreator.addAnnotation(ApplicationScoped.class);
- methodsImplementor.implementListIterable(classCreator, resourceType);
+ methodsImplementor.implementIterable(classCreator, resourceType);
methodsImplementor.implementList(classCreator, resourceType);
- methodsImplementor.implementListSort(classCreator, resourceType);
- methodsImplementor.implementListPaged(classCreator, resourceType);
+ methodsImplementor.implementPagedList(classCreator, resourceType);
methodsImplementor.implementAddList(classCreator, resourceType);
methodsImplementor.implementListById(classCreator, resourceType);
methodsImplementor.implementListPageCount(classCreator, resourceType);
diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java
index 8024240813660..8edd59aa96541 100644
--- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java
+++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java
@@ -15,16 +15,14 @@ public interface ResourceMethodsImplementor {
void implementList(ClassCreator classCreator, String repositoryInterface);
- void implementListIterable(ClassCreator classCreator, String repositoryInterface);
+ void implementIterable(ClassCreator classCreator, String repositoryInterface);
- void implementListPaged(ClassCreator classCreator, String repositoryInterface);
+ void implementPagedList(ClassCreator classCreator, String repositoryInterface);
void implementListPageCount(ClassCreator classCreator, String repositoryInterface);
void implementListById(ClassCreator classCreator, String repositoryInterface);
- public void implementListSort(ClassCreator classCreator, String repositoryInterface);
-
void implementGet(ClassCreator classCreator, String repositoryInterface);
void implementAdd(ClassCreator classCreator, String repositoryInterface);
diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java
index 8c6122950b36b..32b7b50b0371e 100644
--- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java
+++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java
@@ -111,7 +111,6 @@ private void implementResources(Capabilities capabilities,
BuildProducer unremovableBeansProducer,
ResourceMethodsImplementor methodsImplementor,
IndexView index,
- // ResourcePropertiesProvider propertiesProvider,
List repositoriesToImplement) {
ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer);
ResourceImplementor resourceImplementor = new ResourceImplementor(methodsImplementor);
diff --git a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java
new file mode 100644
index 0000000000000..571df125d78d7
--- /dev/null
+++ b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java
@@ -0,0 +1,8 @@
+package io.quarkus.spring.data.rest;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import io.quarkus.spring.data.rest.paged.Record;
+
+public interface JpaRecordsRepository extends JpaRepository {
+}
diff --git a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java
new file mode 100644
index 0000000000000..c5caf26481a44
--- /dev/null
+++ b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java
@@ -0,0 +1,438 @@
+package io.quarkus.spring.data.rest;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.ws.rs.core.Link;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.spring.data.rest.paged.Record;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.http.Header;
+import io.restassured.http.Headers;
+import io.restassured.path.json.JsonPath;
+import io.restassured.response.Response;
+
+class JpaResourceTest {
+ @RegisterExtension
+ static final QuarkusUnitTest TEST = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(AbstractEntity.class, Record.class, JpaRecordsRepository.class)
+ .addAsResource("application.properties")
+ .addAsResource("import.sql"));
+
+ @Test
+ void shouldGet() {
+ given().accept("application/json")
+ .when().get("/jpa-records/1")
+ .then().statusCode(200)
+ .and().body("id", is(equalTo(1)))
+ .and().body("name", is(equalTo("first")));
+ }
+
+ @Test
+ void shouldNotGetNonExistent() {
+ given().accept("application/json")
+ .when().get("/jpa-records/1000")
+ .then().statusCode(404);
+ }
+
+ @Test
+ void shouldGetHal() {
+ given().accept("application/hal+json")
+ .when().get("/jpa-records/1")
+ .then().statusCode(200)
+ .and().body("id", is(equalTo(1)))
+ .and().body("name", is(equalTo("first")))
+ .and().body("_links.add.href", endsWith("/jpa-records"))
+ .and().body("_links.list.href", endsWith("/jpa-records"))
+ .and().body("_links.self.href", endsWith("/jpa-records/1"))
+ .and().body("_links.update.href", endsWith("/jpa-records/1"))
+ .and().body("_links.remove.href", endsWith("/jpa-records/1"));
+ }
+
+ @Test
+ void shouldNotGetNonExistentHal() {
+ given().accept("application/hal+json")
+ .when().get("/jpa-records/1000")
+ .then().statusCode(404);
+ }
+
+ @Test
+ void shouldList() {
+ Response response = given().accept("application/json")
+ .when().get("/jpa-records")
+ .thenReturn();
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body().jsonPath().getList("id")).contains(1, 2);
+ assertThat(response.body().jsonPath().getList("name")).contains("first", "second");
+
+ Map expectedLinks = new HashMap<>(2);
+ expectedLinks.put("first", "/jpa-records?page=0&size=20");
+ expectedLinks.put("last", "/jpa-records?page=0&size=20");
+ assertLinks(response.headers(), expectedLinks);
+ }
+
+ @Test
+ void shouldListHal() {
+ given().accept("application/hal+json")
+ .when().get("/jpa-records")
+ .then().statusCode(200).log().all()
+ .and().body("_embedded.jpa-records.id", hasItems(1, 2))
+ .and().body("_embedded.jpa-records.name", hasItems("first", "second"))
+ .and()
+ .body("_embedded.jpa-records._links.add.href",
+ hasItems(endsWith("/jpa-records"), endsWith("/jpa-records")))
+ .and()
+ .body("_embedded.jpa-records._links.list.href",
+ hasItems(endsWith("/jpa-records"), endsWith("/jpa-records")))
+ .and()
+ .body("_embedded.jpa-records._links.self.href",
+ hasItems(endsWith("/jpa-records/1"), endsWith("/jpa-records/2")))
+ .and()
+ .body("_embedded.jpa-records._links.update.href",
+ hasItems(endsWith("/jpa-records/1"), endsWith("/jpa-records/2")))
+ .and()
+ .body("_embedded.jpa-records._links.remove.href",
+ hasItems(endsWith("/jpa-records/1"), endsWith("/jpa-records/2")))
+ .and().body("_links.add.href", endsWith("/jpa-records"))
+ .and().body("_links.list.href", endsWith("/jpa-records"))
+ .and().body("_links.first.href", endsWith("/jpa-records?page=0&size=20"))
+ .and().body("_links.last.href", endsWith("/jpa-records?page=0&size=20"));
+ }
+
+ @Test
+ void shouldListFirstPage() {
+ Response initResponse = given().accept("application/json")
+ .when().get("/jpa-records")
+ .thenReturn();
+ List ids = initResponse.body().jsonPath().getList("id");
+ List names = initResponse.body().jsonPath().getList("name");
+ int lastPage = ids.size() - 1;
+
+ Response response = given().accept("application/json")
+ .and().queryParam("page", 0)
+ .and().queryParam("size", 1)
+ .when().get("/jpa-records")
+ .thenReturn();
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body().jsonPath().getList("id")).containsOnly(ids.get(0));
+ assertThat(response.body().jsonPath().getList("name")).containsOnly(names.get(0));
+
+ Map expectedLinks = new HashMap<>(3);
+ expectedLinks.put("first", "/jpa-records?page=0&size=1");
+ expectedLinks.put("last", "/jpa-records?page=" + lastPage + "&size=1");
+ expectedLinks.put("next", "/jpa-records?page=1&size=1");
+ assertLinks(response.headers(), expectedLinks);
+ }
+
+ @Test
+ void shouldListFirstPageHal() {
+ Response initResponse = given().accept("application/json")
+ .when().get("/jpa-records")
+ .thenReturn();
+ List ids = initResponse.body().jsonPath().getList("id");
+ List names = initResponse.body().jsonPath().getList("name");
+ int lastPage = ids.size() - 1;
+
+ given().accept("application/hal+json")
+ .and().queryParam("page", 0)
+ .and().queryParam("size", 1)
+ .when().get("/jpa-records")
+ .then().statusCode(200)
+ .and().body("_embedded.jpa-records.id", contains(ids.get(0)))
+ .and().body("_embedded.jpa-records.name", contains(names.get(0)))
+ .and()
+ .body("_embedded.jpa-records._links.add.href",
+ hasItems(endsWith("/jpa-records"), endsWith("/jpa-records")))
+ .and()
+ .body("_embedded.jpa-records._links.list.href",
+ hasItems(endsWith("/jpa-records"), endsWith("/jpa-records")))
+ .and()
+ .body("_embedded.jpa-records._links.self.href",
+ contains(endsWith("/jpa-records/" + ids.get(0))))
+ .and()
+ .body("_embedded.jpa-records._links.update.href",
+ contains(endsWith("/jpa-records/" + ids.get(0))))
+ .and()
+ .body("_embedded.jpa-records._links.remove.href",
+ contains(endsWith("/jpa-records/" + ids.get(0))))
+ .and().body("_links.add.href", endsWith("/jpa-records"))
+ .and().body("_links.list.href", endsWith("/jpa-records"))
+ .and().body("_links.first.href", endsWith("/jpa-records?page=0&size=1"))
+ .and().body("_links.last.href", endsWith("/jpa-records?page=" + lastPage + "&size=1"))
+ .and().body("_links.next.href", endsWith("/jpa-records?page=1&size=1"));
+ }
+
+ @Test
+ void shouldListLastPage() {
+ Response initResponse = given().accept("application/json")
+ .when().get("/jpa-records")
+ .thenReturn();
+ List ids = initResponse.body().jsonPath().getList("id");
+ List names = initResponse.body().jsonPath().getList("name");
+ int lastPage = ids.size() - 1;
+
+ Response response = given().accept("application/json")
+ .and().queryParam("page", lastPage)
+ .and().queryParam("size", 1)
+ .when().get("/jpa-records")
+ .thenReturn();
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ assertThat(response.body().jsonPath().getList("id")).containsOnly(ids.get(lastPage));
+ assertThat(response.body().jsonPath().getList("name")).containsOnly(names.get(lastPage));
+
+ Map expectedLinks = new HashMap<>(3);
+ expectedLinks.put("first", "/jpa-records?page=0&size=1");
+ expectedLinks.put("last", "/jpa-records?page=" + lastPage + "&size=1");
+ expectedLinks.put("previous", "/jpa-records?page=" + (lastPage - 1) + "&size=1");
+ assertLinks(response.headers(), expectedLinks);
+ }
+
+ @Test
+ void shouldListLastPageHal() {
+ Response initResponse = given().accept("application/json")
+ .when().get("/jpa-records")
+ .thenReturn();
+ List ids = initResponse.body().jsonPath().getList("id");
+ List names = initResponse.body().jsonPath().getList("name");
+ int lastPage = ids.size() - 1;
+
+ given().accept("application/hal+json")
+ .and().queryParam("page", lastPage)
+ .and().queryParam("size", 1)
+ .when().get("/jpa-records")
+ .then().statusCode(200)
+ .and().body("_embedded.jpa-records.id", contains(ids.get(lastPage)))
+ .and().body("_embedded.jpa-records.name", contains(names.get(lastPage)))
+ .and()
+ .body("_embedded.jpa-records._links.add.href",
+ hasItems(endsWith("/jpa-records"), endsWith("/jpa-records")))
+ .and()
+ .body("_embedded.jpa-records._links.list.href",
+ hasItems(endsWith("/jpa-records"), endsWith("/jpa-records")))
+ .and()
+ .body("_embedded.jpa-records._links.self.href",
+ contains(endsWith("/jpa-records/" + ids.get(lastPage))))
+ .and()
+ .body("_embedded.jpa-records._links.update.href",
+ contains(endsWith("/jpa-records/" + ids.get(lastPage))))
+ .and()
+ .body("_embedded.jpa-records._links.remove.href",
+ contains(endsWith("/jpa-records/" + ids.get(lastPage))))
+ .and().body("_links.add.href", endsWith("/jpa-records"))
+ .and().body("_links.list.href", endsWith("/jpa-records"))
+ .and().body("_links.first.href", endsWith("/jpa-records?page=0&size=1"))
+ .and().body("_links.last.href", endsWith("/jpa-records?page=" + lastPage + "&size=1"))
+ .and().body("_links.previous.href", endsWith("/jpa-records?page=" + (lastPage - 1) + "&size=1"));
+ }
+
+ @Test
+ void shouldNotGetNonExistentPage() {
+ given().accept("application/json")
+ .and().queryParam("page", 100)
+ .when().get("/jpa-records")
+ .then().statusCode(200)
+ .and().body("id", is(empty()));
+ }
+
+ @Test
+ void shouldNotGetNegativePageOrSize() {
+ given().accept("application/json")
+ .and().queryParam("page", -1)
+ .and().queryParam("size", -1)
+ .when().get("/jpa-records")
+ .then().statusCode(200)
+ // Invalid page and size parameters are replaced with defaults
+ .and().body("id", hasItems(1, 2));
+ }
+
+ @Test
+ void shouldListAscending() {
+ Response response = given().accept("application/json")
+ .when().get("/jpa-records?sort=name,id")
+ .thenReturn();
+
+ List actualNames = response.body().jsonPath().getList("name");
+ List expectedNames = new LinkedList<>(actualNames);
+ expectedNames.sort(Comparator.naturalOrder());
+ assertThat(actualNames).isEqualTo(expectedNames);
+ }
+
+ @Test
+ void shouldListDescending() {
+ Response response = given().accept("application/json")
+ .when().get("/jpa-records?sort=-name,id")
+ .thenReturn();
+
+ List actualNames = response.body().jsonPath().getList("name");
+ List expectedNames = new LinkedList<>(actualNames);
+ expectedNames.sort(Comparator.reverseOrder());
+ assertThat(actualNames).isEqualTo(expectedNames);
+ }
+
+ @Test
+ void shouldCreate() {
+ Response response = given().accept("application/json")
+ .and().contentType("application/json")
+ .and().body("{\"name\": \"test-create\"}")
+ .when().post("/jpa-records")
+ .thenReturn();
+ assertThat(response.statusCode()).isEqualTo(201);
+
+ String location = response.header("Location");
+ int id = Integer.parseInt(location.substring(response.header("Location").lastIndexOf("/") + 1));
+ JsonPath body = response.body().jsonPath();
+ assertThat(body.getInt("id")).isEqualTo(id);
+ assertThat(body.getString("name")).isEqualTo("test-create");
+
+ given().accept("application/json")
+ .when().get(location)
+ .then().statusCode(200)
+ .and().body("id", is(equalTo(id)))
+ .and().body("name", is(equalTo("test-create")));
+ }
+
+ @Test
+ void shouldCreateHal() {
+ Response response = given().accept("application/hal+json")
+ .and().contentType("application/json")
+ .and().body("{\"name\": \"test-create-hal\"}")
+ .when().post("/jpa-records")
+ .thenReturn();
+ assertThat(response.statusCode()).isEqualTo(201);
+
+ String location = response.header("Location");
+ int id = Integer.parseInt(location.substring(response.header("Location").lastIndexOf("/") + 1));
+ JsonPath body = response.body().jsonPath();
+ assertThat(body.getInt("id")).isEqualTo(id);
+ assertThat(body.getString("name")).isEqualTo("test-create-hal");
+ assertThat(body.getString("_links.add.href")).endsWith("/jpa-records");
+ assertThat(body.getString("_links.list.href")).endsWith("/jpa-records");
+ assertThat(body.getString("_links.self.href")).endsWith("/jpa-records/" + id);
+ assertThat(body.getString("_links.update.href")).endsWith("/jpa-records/" + id);
+ assertThat(body.getString("_links.remove.href")).endsWith("/jpa-records/" + id);
+
+ given().accept("application/json")
+ .when().get(location)
+ .then().statusCode(200)
+ .and().body("id", is(equalTo(id)))
+ .and().body("name", is(equalTo("test-create-hal")));
+ }
+
+ @Test
+ void shouldCreateAndUpdate() {
+ Response createResponse = given().accept("application/json")
+ .and().contentType("application/json")
+ .and().body("{\"id\": \"101\", \"name\": \"test-update-create\"}")
+ .when().put("/jpa-records/101")
+ .thenReturn();
+ assertThat(createResponse.statusCode()).isEqualTo(201);
+
+ String location = createResponse.header("Location");
+ int id = Integer.parseInt(location.substring(createResponse.header("Location").lastIndexOf("/") + 1));
+ JsonPath body = createResponse.body().jsonPath();
+ assertThat(body.getInt("id")).isEqualTo(id);
+ assertThat(body.getString("name")).isEqualTo("test-update-create");
+
+ given().accept("application/json")
+ .and().contentType("application/json")
+ .and().body("{\"id\": \"" + id + "\", \"name\": \"test-update\"}")
+ .when().put(location)
+ .then()
+ .statusCode(204);
+ given().accept("application/json")
+ .when().get(location)
+ .then().statusCode(200)
+ .and().body("id", is(equalTo(id)))
+ .and().body("name", is(equalTo("test-update")));
+ }
+
+ @Test
+ void shouldCreateAndUpdateHal() {
+ Response createResponse = given().accept("application/hal+json")
+ .and().contentType("application/json")
+ .and().body("{\"id\": \"102\", \"name\": \"test-update-create-hal\"}")
+ .when().put("/jpa-records/102")
+ .thenReturn();
+ assertThat(createResponse.statusCode()).isEqualTo(201);
+
+ String location = createResponse.header("Location");
+ int id = Integer.parseInt(location.substring(createResponse.header("Location").lastIndexOf("/") + 1));
+ JsonPath body = createResponse.body().jsonPath();
+ assertThat(body.getInt("id")).isEqualTo(id);
+ assertThat(body.getString("name")).isEqualTo("test-update-create-hal");
+ assertThat(body.getString("_links.add.href")).endsWith("/jpa-records");
+ assertThat(body.getString("_links.list.href")).endsWith("/jpa-records");
+ assertThat(body.getString("_links.self.href")).endsWith("/jpa-records/" + id);
+ assertThat(body.getString("_links.update.href")).endsWith("/jpa-records/" + id);
+ assertThat(body.getString("_links.remove.href")).endsWith("/jpa-records/" + id);
+
+ given().accept("application/json")
+ .and().contentType("application/json")
+ .and().body("{\"id\": \"" + id + "\", \"name\": \"test-update-hal\"}")
+ .when().put(location)
+ .then()
+ .statusCode(204);
+ given().accept("application/json")
+ .when().get(location)
+ .then().statusCode(200)
+ .and().body("id", is(equalTo(id)))
+ .and().body("name", is(equalTo("test-update-hal")));
+ }
+
+ @Test
+ void shouldCreateAndDelete() {
+ Response createResponse = given().accept("application/json")
+ .and().contentType("application/json")
+ .and().body("{\"name\": \"test-delete\"}")
+ .when().post("/jpa-records")
+ .thenReturn();
+ assertThat(createResponse.statusCode()).isEqualTo(201);
+
+ String location = createResponse.header("Location");
+ when().delete(location)
+ .then().statusCode(204);
+ when().get(location)
+ .then().statusCode(404);
+ }
+
+ @Test
+ void shouldNotDeleteNonExistent() {
+ when().delete("/jpa-records/1000")
+ .then().statusCode(404);
+ }
+
+ private void assertLinks(Headers headers, Map expectedLinks) {
+ List links = new LinkedList<>();
+ for (Header header : headers.getList("Link")) {
+ links.add(Link.valueOf(header.getValue()));
+ }
+ assertThat(links).hasSize(expectedLinks.size());
+ for (Map.Entry expectedLink : expectedLinks.entrySet()) {
+ assertThat(links).anySatisfy(link -> {
+ assertThat(link.getUri().toString()).endsWith(expectedLink.getValue());
+ assertThat(link.getRel()).isEqualTo(expectedLink.getKey());
+ });
+ }
+ }
+
+}
diff --git a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java
deleted file mode 100644
index a110521962bd2..0000000000000
--- a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package io.quarkus.spring.data.rest.paged;
-
-import static io.restassured.RestAssured.given;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.hamcrest.Matchers.endsWith;
-import static org.hamcrest.Matchers.hasItems;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import jakarta.ws.rs.core.Link;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import io.quarkus.spring.data.rest.AbstractEntity;
-import io.quarkus.spring.data.rest.CrudAndPagedRecordsRepository;
-import io.quarkus.test.QuarkusUnitTest;
-import io.restassured.http.Header;
-import io.restassured.http.Headers;
-
-class DefaultPagedResourceBisTest {
- @RegisterExtension
- static final QuarkusUnitTest TEST = new QuarkusUnitTest()
- .withApplicationRoot((jar) -> jar
- .addClasses(AbstractEntity.class, Record.class, CrudAndPagedRecordsRepository.class)
- .addAsResource("application.properties")
- .addAsResource("import.sql"));
-
- @Test
- // @Disabled
- void shouldListHal() {
- given().accept("application/hal+json")
- .when().get("/crud-and-paged-records")
- .then().statusCode(200).log().all()
- .and().body("_embedded.crud-and-paged-records.id", hasItems(1, 2))
- .and().body("_embedded.crud-and-paged-records.name", hasItems("first", "second"))
- .and()
- .body("_embedded.crud-and-paged-records._links.add.href",
- hasItems(endsWith("/crud-and-paged-records"), endsWith("/crud-and-paged-records")))
- .and()
- .body("_embedded.crud-and-paged-records._links.list.href",
- hasItems(endsWith("/crud-and-paged-records"), endsWith("/crud-and-paged-records")))
- .and()
- .body("_embedded.crud-and-paged-records._links.self.href",
- hasItems(endsWith("/crud-and-paged-records/1"), endsWith("/crud-and-paged-records/2")))
- .and()
- .body("_embedded.crud-and-paged-records._links.update.href",
- hasItems(endsWith("/crud-and-paged-records/1"), endsWith("/crud-and-paged-records/2")))
- .and()
- .body("_embedded.crud-and-paged-records._links.remove.href",
- hasItems(endsWith("/crud-and-paged-records/1"), endsWith("/crud-and-paged-records/2")))
- .and().body("_links.add.href", endsWith("/crud-and-paged-records"))
- .and().body("_links.list.href", endsWith("/crud-and-paged-records"))
- .and().body("_links.first.href", endsWith("/crud-and-paged-records?page=0&size=20"))
- .and().body("_links.last.href", endsWith("/crud-and-paged-records?page=0&size=20"));
- }
-
- private void assertLinks(Headers headers, Map expectedLinks) {
- List links = new LinkedList<>();
- for (Header header : headers.getList("Link")) {
- links.add(Link.valueOf(header.getValue()));
- }
- assertThat(links).hasSize(expectedLinks.size());
- for (Map.Entry expectedLink : expectedLinks.entrySet()) {
- assertThat(links).anySatisfy(link -> {
- assertThat(link.getUri().toString()).endsWith(expectedLink.getValue());
- assertThat(link.getRel()).isEqualTo(expectedLink.getKey());
- });
- }
- }
-}
diff --git a/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java
new file mode 100644
index 0000000000000..b4ef189219470
--- /dev/null
+++ b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java
@@ -0,0 +1,73 @@
+package io.quarkus.it.spring.data.rest;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.validation.constraints.NotBlank;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+
+@Entity
+public class Article {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ // @Length(min = 2, max = 50, message = "length must be between {min} and {max}")
+ @NotBlank(message = "Name may not be blank")
+ private String name;
+
+ // @Length(min = 2, max = 50, message = "length must be between {min} and {max}")
+ @NotBlank(message = "Author may not be blank")
+ private String author;
+
+ @JsonBackReference
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "library_id")
+ private Library library;
+
+ public Article() {
+ }
+
+ public Article(String name, String author) {
+ this.name = name;
+ this.author = author;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public Library getLibrary() {
+ return library;
+ }
+
+ public void setLibrary(Library library) {
+ this.library = library;
+ }
+}
diff --git a/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java
new file mode 100644
index 0000000000000..2dc5bd7c91988
--- /dev/null
+++ b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java
@@ -0,0 +1,6 @@
+package io.quarkus.it.spring.data.rest;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ArticleJpaRepository extends JpaRepository {
+}
diff --git a/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java
new file mode 100644
index 0000000000000..d1d07a1371725
--- /dev/null
+++ b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java
@@ -0,0 +1,60 @@
+package io.quarkus.it.spring.data.rest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import jakarta.validation.constraints.NotBlank;
+
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
+@Entity
+public class Library {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @NotBlank(message = "Name may not be blank")
+ private String name;
+
+ @JsonManagedReference
+ @OneToMany(mappedBy = "library", cascade = CascadeType.ALL)
+ private List articles = new ArrayList<>();
+
+ public Library() {
+ }
+
+ public Library(String name) {
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getArticles() {
+ return articles;
+ }
+
+ public void setArticles(List articles) {
+ this.articles = articles;
+ }
+}
diff --git a/integration-tests/spring-data-rest/src/main/resources/import.sql b/integration-tests/spring-data-rest/src/main/resources/import.sql
index 0fefc273330cc..4769c879ec1b4 100644
--- a/integration-tests/spring-data-rest/src/main/resources/import.sql
+++ b/integration-tests/spring-data-rest/src/main/resources/import.sql
@@ -4,3 +4,10 @@ alter sequence Author_SEQ restart with 2;
insert into book(id, title, author_id) values (1, 'Crime and Punishment', 1);
insert into book(id, title, author_id) values (2, 'Idiot', 1);
alter sequence Book_SEQ restart with 3;
+
+INSERT INTO library(name) VALUES('Library1');
+
+INSERT INTO article(name, author, library_id) VALUES ('Aeneid','Virgil', 1);
+INSERT INTO article(name, author, library_id) VALUES ('Beach House','James Patterson',1);
+INSERT INTO article(name, author) VALUES ('Cadillac Desert','Marc Reisner');
+INSERT INTO article(name, author) VALUES ('Dagon and Other Macabre Tales','H.P. Lovecraft ');
diff --git a/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java b/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java
index c1d1e08bf2f66..caa599b4af964 100644
--- a/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java
+++ b/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java
@@ -7,13 +7,25 @@
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
import jakarta.json.Json;
import jakarta.json.JsonObject;
+import jakarta.ws.rs.core.Link;
+import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.Header;
+import io.restassured.http.Headers;
import io.restassured.response.Response;
@QuarkusTest
@@ -33,6 +45,9 @@ class SpringDataRestTest {
private static final String IDIOT_TITLE = "Idiot";
+ protected static final List ORIGINAL_ARTICLES = Arrays.asList("Aeneid", "Beach House", "Cadillac Desert",
+ "Dagon and Other Macabre Tales");
+
@Test
void shouldGetAuthor() {
given().accept("application/json")
@@ -245,4 +260,37 @@ void shouldNotUpdateBookWithBlankTitle() {
.and().body("parameterViolations[0].path", equalTo("update.entity.title"))
.and().body("parameterViolations[0].message", equalTo("must not be blank"));
}
+
+ @Test
+ void sorting() {
+ //Test repository sorting
+ List articleNamesSortedDesc = new ArrayList<>(getItemsAfterUpdates());
+ articleNamesSortedDesc.sort(Comparator.reverseOrder());
+ Response response = given()
+ .accept("application/json")
+ .queryParam("sort", "-name")
+ .when().get("/article-jpa")
+ .then()
+ .statusCode(HttpStatus.SC_OK).extract().response();
+ List articleNamesRepositorySortedDesc = response.jsonPath().getList("name");
+ assertEquals(articleNamesSortedDesc, articleNamesRepositorySortedDesc);
+ }
+
+ protected List getItemsAfterUpdates() {
+ return ORIGINAL_ARTICLES;
+ }
+
+ private void assertLinks(Headers headers, Map expectedLinks) {
+ List links = new LinkedList<>();
+ for (Header header : headers.getList("Link")) {
+ links.add(Link.valueOf(header.getValue()));
+ }
+ assertThat(links).hasSize(expectedLinks.size());
+ for (Map.Entry expectedLink : expectedLinks.entrySet()) {
+ assertThat(links).anySatisfy(link -> {
+ assertThat(link.getUri().toString()).endsWith(expectedLink.getValue());
+ assertThat(link.getRel()).isEqualTo(expectedLink.getKey());
+ });
+ }
+ }
}