diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 0d316c8b..eda8d607 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -22,6 +22,7 @@ import java.util.List; import feign.Contract; import feign.Feign; import feign.Logger; +import feign.QueryMapEncoder; import feign.Retryer; import feign.codec.Decoder; import feign.codec.Encoder; @@ -47,6 +48,7 @@ import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; import org.springframework.cloud.openfeign.support.AbstractFormWriter; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.PageableSpringEncoder; +import org.springframework.cloud.openfeign.support.PageableSpringQueryMapEncoder; import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; import org.springframework.cloud.openfeign.support.SpringDecoder; import org.springframework.cloud.openfeign.support.SpringEncoder; @@ -67,6 +69,7 @@ import static feign.form.ContentType.MULTIPART; * @author Darren Foong * @author Jonatan Ivanov * @author Olga Maciaszek-Sharma + * @author Hyeonmin Park */ @Configuration(proxyBeanMethods = false) public class FeignClientsConfiguration { @@ -119,6 +122,13 @@ public class FeignClientsConfiguration { return encoder; } + @Bean + @ConditionalOnClass(name = "org.springframework.data.domain.Pageable") + @ConditionalOnMissingBean + public QueryMapEncoder feignQueryMapEncoderPageable() { + return new PageableSpringQueryMapEncoder(); + } + @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { 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 new file mode 100644 index 00000000..0a2171b6 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2021 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.openfeign.support; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import feign.querymap.BeanQueryMapEncoder; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * Provides support for encoding Pageable annotated as + * {@link org.springframework.cloud.openfeign.SpringQueryMap}. + * + * @author Hyeonmin Park + * @since 2.2.8 + */ +public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder { + + @Override + public Map encode(Object object) { + if (supports(object)) { + Map queryMap = new HashMap<>(); + + if (object instanceof Pageable) { + Pageable pageable = (Pageable) object; + + if (pageable.isPaged()) { + queryMap.put("page", pageable.getPageNumber()); + queryMap.put("size", pageable.getPageSize()); + } + + if (pageable.getSort() != null) { + applySort(queryMap, pageable.getSort()); + } + } + else if (object instanceof Sort) { + Sort sort = (Sort) object; + applySort(queryMap, sort); + } + return queryMap; + } + else { + return super.encode(object); + } + } + + private void applySort(Map queryMap, Sort sort) { + List sortQueries = new ArrayList<>(); + for (Sort.Order order : sort) { + sortQueries.add(order.getProperty() + "%2C" + order.getDirection()); + } + if (!sortQueries.isEmpty()) { + queryMap.put("sort", sortQueries); + } + } + + protected boolean supports(Object object) { + return object instanceof Pageable || object instanceof Sort; + } + +} 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 e856a68c..ecc52e37 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 @@ -66,7 +66,6 @@ public class FeignPageableEncodingTests { @Test public void testPageable() { - // given Pageable pageable = PageRequest.of(0, 10, Sort.Direction.ASC, "sortProperty"); @@ -85,7 +84,6 @@ public class FeignPageableEncodingTests { assertThat(order.getDirection()).isEqualTo(Sort.Direction.ASC); assertThat(order.getProperty()).isEqualTo("sortProperty"); } - } @Test @@ -112,7 +110,6 @@ public class FeignPageableEncodingTests { Sort.Order order = optionalOrder.get(); assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); assertThat(order.getProperty()).isEqualTo("sortProperty"); - } @Test @@ -143,7 +140,131 @@ public class FeignPageableEncodingTests { Sort.Order secondOrder = orderList.get(1); assertThat(secondOrder.getDirection()).isEqualTo(Sort.Direction.ASC); assertThat(secondOrder.getProperty()).isEqualTo("sortProperty2"); + } + @Test + public void testPageableWithoutSort() { + // given + Pageable pageable = PageRequest.of(0, 10); + + // 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().isSorted()).isFalse(); + + List invoiceList = response.getBody().getContent(); + assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1); + } + + @Test + public void testPageableWithoutSortWithBody() { + // given + Pageable pageable = PageRequest.of(0, 10); + + // 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"); + } + + @Test + public void testPageableWithBody() { + // given + Pageable pageable = PageRequest.of(0, 10, + Sort.by(Sort.Order.desc("sortProperty1"), Sort.Order.asc("sortProperty2"))); + + // 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("sortProperty1"); + + Sort.Order secondOrder = orderList.get(1); + assertThat(secondOrder.getDirection()).isEqualTo(Sort.Direction.ASC); + assertThat(secondOrder.getProperty()).isEqualTo("sortProperty2"); + } + + @Test + public void testUnpagedWithBody() { + // given + Pageable unpaged = Pageable.unpaged(); + + // when + final ResponseEntity> response = this.invoiceClient.getInvoicesPagedWithBody(unpaged, + "InvoiceTitleFromBody"); + + // then + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + + List invoiceList = response.getBody().getContent(); + assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1); + + Invoice firstInvoice = invoiceList.get(0); + assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody"); + } + + @Test + public void testSortWithBody() { + // given + Sort sort = Sort.by(Sort.Order.desc("amount")); + + // 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()); + + 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()); + } } @EnableFeignClients(clients = InvoiceClient.class) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/client/InvoiceClient.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/client/InvoiceClient.java index eb18441f..57d629e0 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/client/InvoiceClient.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/client/InvoiceClient.java @@ -19,10 +19,12 @@ package org.springframework.cloud.openfeign.encoding.app.client; import java.util.List; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; import org.springframework.cloud.openfeign.encoding.app.domain.Invoice; import org.springframework.data.domain.Page; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -30,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestMethod; * Simple Feign client for retrieving the invoice list. * * @author Jakub Narloch + * @author Hyeonmin Park */ @FeignClient("local") public interface InvoiceClient { @@ -37,6 +40,16 @@ public interface InvoiceClient { @RequestMapping(value = "invoicesPaged", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity> getInvoicesPaged(org.springframework.data.domain.Pageable pageable); + @RequestMapping(value = "invoicesPagedWithBody", method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity> getInvoicesPagedWithBody( + @SpringQueryMap org.springframework.data.domain.Pageable pageable, @RequestBody String titlePrefix); + + @RequestMapping(value = "invoicesSortedWithBody", method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity> getInvoicesSortedWithBody(@SpringQueryMap org.springframework.data.domain.Sort sort, + @RequestBody String titlePrefix); + @RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity> getInvoices(); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/resource/InvoiceResource.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/resource/InvoiceResource.java index ad0a0113..5eb6a7ce 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/resource/InvoiceResource.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/app/resource/InvoiceResource.java @@ -18,12 +18,14 @@ package org.springframework.cloud.openfeign.encoding.app.resource; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Locale; import org.springframework.cloud.openfeign.encoding.app.domain.Invoice; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; @@ -35,6 +37,7 @@ import org.springframework.web.bind.annotation.RestController; * An sample REST controller, that potentially returns large response - used for testing. * * @author Jakub Narloch + * @author Hyeonmin Park */ @RestController public class InvoiceResource { @@ -42,7 +45,7 @@ public class InvoiceResource { @RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getInvoices() { - return ResponseEntity.ok(createInvoiceList(100)); + return ResponseEntity.ok(createInvoiceList(null, 100, null)); } @RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, @@ -54,18 +57,69 @@ public class InvoiceResource { @RequestMapping(value = "invoicesPaged", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getInvoicesPaged(org.springframework.data.domain.Pageable pageable) { - Page page = new PageImpl<>(createInvoiceList(pageable.getPageSize()), pageable, 100); + Page page = new PageImpl<>(createInvoiceList(null, pageable.getPageSize(), pageable.getSort()), + pageable, 100); return ResponseEntity.ok(page); } - private List createInvoiceList(int count) { + @RequestMapping(value = "invoicesPagedWithBody", method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getInvoicesPagedWithBody(org.springframework.data.domain.Pageable pageable, + @RequestBody String titlePrefix) { + Page page = new PageImpl<>(createInvoiceList(titlePrefix, pageable.getPageSize(), pageable.getSort()), + pageable, 100); + return ResponseEntity.ok(page); + } + + @RequestMapping(value = "invoicesSortedWithBody", method = RequestMethod.POST, + consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getInvoicesSortedWithBody(org.springframework.data.domain.Sort sort, + @RequestBody String titlePrefix) { + Page page = new PageImpl<>(createInvoiceList(titlePrefix, 100, sort), PageRequest.of(0, 100, sort), + 100); + return ResponseEntity.ok(page); + } + + private List createInvoiceList(String titlePrefix, int count, org.springframework.data.domain.Sort sort) { + if (titlePrefix == null) { + titlePrefix = "Invoice"; + } final List invoices = new ArrayList<>(); for (int ind = 0; ind < count; ind++) { final Invoice invoice = new Invoice(); - invoice.setTitle("Invoice " + (ind + 1)); + invoice.setTitle(titlePrefix + " " + (ind + 1)); invoice.setAmount(new BigDecimal(String.format(Locale.US, "%.2f", Math.random() * 1000))); invoices.add(invoice); } + if (sort != null) { + Comparator comparatorForSort = null; + for (org.springframework.data.domain.Sort.Order order : sort) { + Comparator comparatorForOrder; + if (order.getProperty().equals("title")) { + comparatorForOrder = Comparator.comparing(Invoice::getTitle); + } + else if (order.getProperty().equals("amount")) { + comparatorForOrder = Comparator.comparing(Invoice::getAmount); + } + else { + continue; + } + + if (order.isDescending()) { + comparatorForOrder = comparatorForOrder.reversed(); + } + + if (comparatorForSort == null) { + comparatorForSort = comparatorForOrder; + } + else { + comparatorForSort = comparatorForSort.thenComparing(comparatorForOrder); + } + } + if (comparatorForSort != null) { + invoices.sort(comparatorForSort); + } + } return invoices; }