From c62ce05bc8dabf454b67e1003a748837074898bd Mon Sep 17 00:00:00 2001 From: Auri Munoz Date: Fri, 14 Jun 2024 11:50:18 +0200 Subject: [PATCH] Include several fixes related to #40344 --- bom/application/pom.xml | 2 +- .../RepositoryMethodsImplementor.java | 52 +-- .../RepositoryPropertiesProvider.java | 3 - .../rest/deployment/ResourceImplementor.java | 5 +- .../ResourceMethodsImplementor.java | 6 +- .../deployment/SpringDataRestProcessor.java | 1 - .../data/rest/JpaRecordsRepository.java | 8 + .../spring/data/rest/JpaResourceTest.java | 438 ++++++++++++++++++ .../paged/DefaultPagedResourceBisTest.java | 73 --- .../quarkus/it/spring/data/rest/Article.java | 73 +++ .../data/rest/ArticleJpaRepository.java | 6 + .../quarkus/it/spring/data/rest/Library.java | 60 +++ .../src/main/resources/import.sql | 7 + .../spring/data/rest/SpringDataRestTest.java | 48 ++ 14 files changed, 661 insertions(+), 121 deletions(-) create mode 100644 extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java create mode 100644 extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java delete mode 100644 extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java create mode 100644 integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java create mode 100644 integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java create mode 100644 integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 862fcb69b8ed6..ffa16a8152b31 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()); + }); + } + } }