diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringEncoder.java index 49f69ec69..dce848824 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ * * @author Pascal Büttiker * @author Yanming Zhou + * @author Gokalp Kuscu */ public class PageableSpringEncoder implements Encoder { @@ -53,6 +54,11 @@ public class PageableSpringEncoder implements Encoder { */ private String sortParameter = "sort"; + /** + * Sort ignoreCase parameter name. + */ + private final String ignoreCase = "ignorecase"; + /** * Creates a new PageableSpringEncoder with the given delegate for fallback. If no * delegate is provided and this encoder cant handle the request, an EncodeException @@ -115,7 +121,11 @@ private void applySort(RequestTemplate template, Sort sort) { } } for (Sort.Order order : sort) { - sortQueries.add(order.getProperty() + "%2C" + order.getDirection()); + String sortQuery = order.getProperty() + "%2C" + order.getDirection(); + if (order.isIgnoreCase()) { + sortQuery += "%2C" + ignoreCase; + } + sortQueries.add(sortQuery); } if (!sortQueries.isEmpty()) { template.query(sortParameter, sortQueries); diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java index a7c853a12..d44d23499 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ * * @author Hyeonmin Park * @author Yanming Zhou + * @author Gokalp Kuscu * @since 2.2.8 */ public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder { @@ -51,6 +52,11 @@ public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder { */ private String sortParameter = "sort"; + /** + * Sort ignoreCase parameter name. + */ + private final String ignoreCase = "ignorecase"; + public void setPageParameter(String pageParameter) { this.pageParameter = pageParameter; } @@ -92,7 +98,11 @@ else if (object instanceof Sort sort) { private void applySort(Map queryMap, Sort sort) { List sortQueries = new ArrayList<>(); for (Sort.Order order : sort) { - sortQueries.add(order.getProperty() + "%2C" + order.getDirection()); + String sortQuery = order.getProperty() + "%2C" + order.getDirection(); + if (order.isIgnoreCase()) { + sortQuery += "%2C" + ignoreCase; + } + sortQueries.add(sortQuery); } if (!sortQueries.isEmpty()) { queryMap.put(sortParameter, sortQueries); diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJsonComponent.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJsonComponent.java index cf22a0dbf..7c454be3c 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJsonComponent.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJsonComponent.java @@ -39,6 +39,7 @@ * * @author Can Bezmen * @author Olga Maciaszek-Sharma + * @author Gokalp Kuscu */ public class SortJsonComponent { @@ -90,8 +91,23 @@ public Class handledType() { private static Sort toSort(ArrayNode arrayNode) { List orders = new ArrayList<>(); for (JsonNode jsonNode : arrayNode) { - Sort.Order order = new Sort.Order(Sort.Direction.valueOf(jsonNode.get("direction").textValue()), - jsonNode.get("property").textValue()); + Sort.Order order; + // there is no way to construct without null handling + if ((jsonNode.has("ignoreCase") && jsonNode.get("ignoreCase").isBoolean()) + && jsonNode.has("nullHandling") && jsonNode.get("nullHandling").isTextual()) { + + boolean ignoreCase = jsonNode.get("ignoreCase").asBoolean(); + String nullHandlingValue = jsonNode.get("nullHandling").textValue(); + + order = new Sort.Order(Sort.Direction.valueOf(jsonNode.get("direction").textValue()), + jsonNode.get("property").textValue(), ignoreCase, + Sort.NullHandling.valueOf(nullHandlingValue)); + } + else { + // backward compatibility + order = new Sort.Order(Sort.Direction.valueOf(jsonNode.get("direction").textValue()), + jsonNode.get("property").textValue()); + } orders.add(order); } return Sort.by(orders); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/FeignPageableEncodingTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/FeignPageableEncodingTests.java index 30fe65990..54b4697d0 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/FeignPageableEncodingTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/FeignPageableEncodingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,6 +53,7 @@ * * @author Charlie Mordant. * @author Hyeonmin Park + * @author Gokalp Kuscu */ @SpringBootTest(classes = FeignPageableEncodingTests.Application.class, webEnvironment = RANDOM_PORT, value = { "spring.cloud.openfeign.compression.request.enabled=true" }) @@ -264,6 +265,110 @@ void testSortWithBody() { } } + @Test + void testPageableWithIgnoreCase() { + // given + Sort.Order anySorting = Sort.Order.asc("anySorting").ignoreCase(); + Pageable pageable = PageRequest.of(0, 10, Sort.by(anySorting)); + + // when + final ResponseEntity> response = this.invoiceClient.getInvoicesPaged(pageable); + + // then + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize()); + assertThat(response.getBody().getPageable().getSort()).hasSize(1); + Optional optionalOrder = response.getBody().getPageable().getSort().get().findFirst(); + assertThat(optionalOrder.isPresent()).isEqualTo(true); + if (optionalOrder.isPresent()) { + Sort.Order order = optionalOrder.get(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.ASC); + assertThat(order.getProperty()).isEqualTo("anySorting"); + assertThat(order.isIgnoreCase()).as("isIgnoreCase does not have expected value").isEqualTo(true); + assertThat(order.getNullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + } + + @Test + void testSortWithIgnoreCaseAndBody() { + // given + Sort.Order anySorting = Sort.Order.desc("amount").ignoreCase(); + Sort sort = Sort.by(anySorting); + + // when + final ResponseEntity> response = this.invoiceClient.getInvoicesSortedWithBody(sort, + "InvoiceTitleFromBody"); + + // then + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(sort).isEqualTo(response.getBody().getSort()); + + Optional optionalOrder = response.getBody().getPageable().getSort().get().findFirst(); + assertThat(optionalOrder.isPresent()).isEqualTo(true); + if (optionalOrder.isPresent()) { + Sort.Order order = optionalOrder.get(); + assertThat(order.isIgnoreCase()).as("isIgnoreCase does not have expected value").isEqualTo(true); + assertThat(order.getNullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + + List invoiceList = response.getBody().getContent(); + assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1); + + Invoice firstInvoice = invoiceList.get(0); + assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody"); + + for (int ind = 0; ind < invoiceList.size() - 1; ind++) { + assertThat(invoiceList.get(ind).getAmount()).isGreaterThanOrEqualTo(invoiceList.get(ind + 1).getAmount()); + } + + } + + @Test + void testPageableMultipleSortPropertiesWithBodyAndIgnoreCase() { + // given + Sort.Order anySorting1 = Sort.Order.desc("anySorting1").ignoreCase(); + Sort.Order anySorting2 = Sort.Order.asc("anySorting2"); + Pageable pageable = PageRequest.of(0, 10, Sort.by(anySorting1, anySorting2)); + + // when + final ResponseEntity> response = this.invoiceClient.getInvoicesPagedWithBody(pageable, + "InvoiceTitleFromBody"); + + // then + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize()); + + List invoiceList = response.getBody().getContent(); + assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1); + + Invoice firstInvoice = invoiceList.get(0); + assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody"); + + Sort sort = response.getBody().getPageable().getSort(); + assertThat(sort).hasSize(2); + + List orderList = sort.toList(); + assertThat(orderList).hasSize(2); + + Sort.Order firstOrder = orderList.get(0); + assertThat(firstOrder.getDirection()).isEqualTo(Sort.Direction.DESC); + assertThat(firstOrder.getProperty()).isEqualTo("anySorting1"); + assertThat(firstOrder.isIgnoreCase()).as("isIgnoreCase does not have expected value").isEqualTo(true); + assertThat(firstOrder.getNullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + + Sort.Order secondOrder = orderList.get(1); + assertThat(secondOrder.getDirection()).isEqualTo(Sort.Direction.ASC); + assertThat(secondOrder.getProperty()).isEqualTo("anySorting2"); + assertThat(secondOrder.isIgnoreCase()).as("isIgnoreCase does not have expected value").isEqualTo(false); + assertThat(secondOrder.getNullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + @EnableFeignClients(clients = InvoiceClient.class) @LoadBalancerClient(name = "local", configuration = LocalClientConfiguration.class) @SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app", diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SortJacksonModuleTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SortJacksonModuleTests.java index 303eec4ab..ae71d328d 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SortJacksonModuleTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SortJacksonModuleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ /** * @author Can Bezmen + * @author Gokalp Kuscu */ @ExtendWith(MockitoExtension.class) class SortJacksonModuleTests { @@ -53,7 +54,7 @@ public void setup() { } @Test - public void deserializePage() throws JsonProcessingException { + public void testDeserializePage() throws JsonProcessingException { // Given String pageJson = "{\"content\":[\"A name\"],\"number\":1,\"size\":2,\"totalElements\":3,\"sort\":[{\"direction\":\"ASC\",\"property\":\"field\",\"ignoreCase\":false,\"nullHandling\":\"NATIVE\",\"descending\":false,\"ascending\":true}]}"; // When @@ -72,11 +73,85 @@ public void deserializePage() throws JsonProcessingException { Sort.Order order = optionalOrder.get(); assertThat(order, hasProperty("property", is("field"))); assertThat(order, hasProperty("direction", is(Sort.Direction.ASC))); + assertThat(order, hasProperty("ignoreCase", is(false))); + assertThat(order, hasProperty("nullHandling", is(Sort.NullHandling.NATIVE))); } } @Test - public void serializePage() throws IOException { + public void testDeserializePageWithoutIgnoreCaseAndNullHandling() throws JsonProcessingException { + // Given + String pageJson = "{\"content\":[\"A name\"],\"number\":1,\"size\":2,\"totalElements\":3,\"sort\":[{\"direction\":\"ASC\",\"property\":\"field\",\"descending\":false,\"ascending\":true}]}"; + // When + Page result = objectMapper.readValue(pageJson, Page.class); + // Then + assertThat(result, notNullValue()); + assertThat(result, hasProperty("totalElements", is(3L))); + assertThat(result.getContent(), hasSize(1)); + assertThat(result.getPageable(), notNullValue()); + assertThat(result.getPageable().getPageNumber(), is(1)); + assertThat(result.getPageable().getPageSize(), is(2)); + assertThat(result.getPageable().getSort(), notNullValue()); + result.getPageable().getSort(); + Optional optionalOrder = result.getPageable().getSort().get().findFirst(); + if (optionalOrder.isPresent()) { + Sort.Order order = optionalOrder.get(); + assertThat(order, hasProperty("property", is("field"))); + assertThat(order, hasProperty("direction", is(Sort.Direction.ASC))); + } + } + + @Test + public void testDeserializePageWithoutNullHandling() throws JsonProcessingException { + // Given + String pageJson = "{\"content\":[\"A name\"],\"number\":1,\"size\":2,\"totalElements\":3,\"sort\":[{\"direction\":\"ASC\",\"property\":\"field\",\"ignoreCase\":true,\"descending\":false,\"ascending\":true}]}"; + // When + Page result = objectMapper.readValue(pageJson, Page.class); + // Then + assertThat(result, notNullValue()); + assertThat(result, hasProperty("totalElements", is(3L))); + assertThat(result.getContent(), hasSize(1)); + assertThat(result.getPageable(), notNullValue()); + assertThat(result.getPageable().getPageNumber(), is(1)); + assertThat(result.getPageable().getPageSize(), is(2)); + assertThat(result.getPageable().getSort(), notNullValue()); + result.getPageable().getSort(); + Optional optionalOrder = result.getPageable().getSort().get().findFirst(); + if (optionalOrder.isPresent()) { + Sort.Order order = optionalOrder.get(); + assertThat(order, hasProperty("property", is("field"))); + assertThat(order, hasProperty("direction", is(Sort.Direction.ASC))); + assertThat(order, hasProperty("ignoreCase", is(false))); + } + } + + @Test + public void testDeserializePageWithTrueMarkedIgnoreCaseAndNullHandling() throws JsonProcessingException { + // Given + String pageJson = "{\"content\":[\"A name\"],\"number\":1,\"size\":2,\"totalElements\":3,\"sort\":[{\"direction\":\"ASC\",\"property\":\"field\",\"ignoreCase\":true,\"nullHandling\":\"NATIVE\",\"descending\":false,\"ascending\":true}]}"; + // When + Page result = objectMapper.readValue(pageJson, Page.class); + // Then + assertThat(result, notNullValue()); + assertThat(result, hasProperty("totalElements", is(3L))); + assertThat(result.getContent(), hasSize(1)); + assertThat(result.getPageable(), notNullValue()); + assertThat(result.getPageable().getPageNumber(), is(1)); + assertThat(result.getPageable().getPageSize(), is(2)); + assertThat(result.getPageable().getSort(), notNullValue()); + result.getPageable().getSort(); + Optional optionalOrder = result.getPageable().getSort().get().findFirst(); + if (optionalOrder.isPresent()) { + Sort.Order order = optionalOrder.get(); + assertThat(order, hasProperty("property", is("field"))); + assertThat(order, hasProperty("direction", is(Sort.Direction.ASC))); + assertThat(order, hasProperty("ignoreCase", is(true))); + assertThat(order, hasProperty("nullHandling", is(Sort.NullHandling.NATIVE))); + } + } + + @Test + public void testSerializePage() throws IOException { // Given Sort sort = Sort.by(Sort.Order.by("fieldName")); // When @@ -86,4 +161,16 @@ public void serializePage() throws IOException { assertThat(result, containsString("\"property\":\"fieldName\"")); } + @Test + public void testSerializePageWithGivenIgnoreCase() throws IOException { + // Given + Sort sort = Sort.by(Sort.Order.by("fieldName"), Sort.Order.by("fieldName2").ignoreCase()); + // When + String result = objectMapper.writeValueAsString(sort); + // Then + assertThat(result, containsString("\"direction\":\"ASC\"")); + assertThat(result, containsString("\"property\":\"fieldName\"")); + assertThat(result, containsString("\"property\":\"fieldName2\",\"ignoreCase\":true")); + } + }