diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 1ea15b0c..edddeb03 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -47,6 +47,9 @@ public interface StoreClient { @RequestMapping(method = RequestMethod.GET, value = "/stores") List getStores(); + @RequestMapping(method = RequestMethod.GET, value = "/stores") + Page getStores(Pageable pageable); + @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") Store update(@PathVariable("storeId") Long storeId, Store store); } 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 9b1c453c..b934e9d9 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 @@ -43,6 +43,7 @@ import org.springframework.cloud.openfeign.support.AbstractFormWriter; import org.springframework.cloud.openfeign.support.PageJacksonModule; import org.springframework.cloud.openfeign.support.PageableSpringEncoder; import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; +import org.springframework.cloud.openfeign.support.SortJacksonModule; import org.springframework.cloud.openfeign.support.SpringDecoder; import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.cloud.openfeign.support.SpringMvcContract; @@ -151,6 +152,12 @@ public class FeignClientsConfiguration { return new PageJacksonModule(); } + @Bean + @ConditionalOnClass(name = "org.springframework.data.domain.Page") + public Module sortModule() { + return new SortJacksonModule(); + } + @Bean @ConditionalOnMissingBean(FeignClientConfigurer.class) public FeignClientConfigurer feignClientConfigurer() { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageJacksonModule.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageJacksonModule.java index c21aa177..72662101 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageJacksonModule.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/PageJacksonModule.java @@ -33,7 +33,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; /** - * This jackson module provides support to deserialize spring {@link Page} objects. + * This Jackson module provides support to deserialize Spring {@link Page} objects. * * @author Pascal Büttiker */ @@ -65,9 +65,17 @@ public class PageJacksonModule extends Module { SimplePageImpl(@JsonProperty("content") List content, @JsonProperty("number") int number, @JsonProperty("size") int size, - @JsonProperty("totalElements") long totalElements) { - delegate = new PageImpl<>(content, PageRequest.of(number, size), - totalElements); + @JsonProperty("totalElements") long totalElements, + @JsonProperty("sort") Sort sort) { + PageRequest pageRequest; + if (sort != null) { + pageRequest = PageRequest.of(number, size, sort); + } + else { + pageRequest = PageRequest.of(number, size); + } + delegate = new PageImpl<>(content, pageRequest, totalElements); + } @JsonProperty diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJacksonModule.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJacksonModule.java new file mode 100644 index 00000000..162a5497 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJacksonModule.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2020 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 com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleSerializers; + +import org.springframework.data.domain.Sort; + +/** + * This Jackson module provides support for serializing and deserializing for Spring + * {@link Sort} object. + * + * @author Can Bezmen + */ +public class SortJacksonModule extends Module { + + @Override + public String getModuleName() { + return "SortModule"; + } + + @Override + public Version version() { + return new Version(0, 1, 0, "", null, null); + } + + @Override + public void setupModule(SetupContext context) { + SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(Sort.class, new SortJsonComponent.SortSerializer()); + context.addSerializers(serializers); + + SimpleDeserializers deserializers = new SimpleDeserializers(); + deserializers.addDeserializer(Sort.class, + new SortJsonComponent.SortDeserializer()); + context.addDeserializers(deserializers); + } + +} 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 new file mode 100644 index 00000000..05c8b58b --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SortJsonComponent.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-2020 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.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.node.ArrayNode; +import feign.codec.EncodeException; + +import org.springframework.data.domain.Sort; + +/** + * This class provides provides support for serializing and deserializing for Spring + * {@link Sort} object. + * + * @author Can Bezmen + */ +public class SortJsonComponent { + + public static class SortSerializer extends JsonSerializer { + + @Override + public void serialize(Sort value, JsonGenerator gen, + SerializerProvider serializers) throws IOException { + gen.writeStartArray(); + value.iterator().forEachRemaining(v -> { + try { + gen.writeObject(v); + } + catch (IOException e) { + throw new EncodeException("Couldn't serialize object " + v); + } + }); + gen.writeEndArray(); + } + + @Override + public Class handledType() { + return Sort.class; + } + + } + + public static class SortDeserializer extends JsonDeserializer { + + @Override + public Sort deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext) throws IOException { + TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser); + if (treeNode.isArray()) { + ArrayNode arrayNode = (ArrayNode) treeNode; + 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()); + orders.add(order); + } + return Sort.by(orders); + } + return null; + } + + @Override + public Class handledType() { + return Sort.class; + } + + } + +} 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 2a49cb77..cc7cfc3f 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.openfeign.encoding; +import java.util.Optional; + import org.junit.Test; import org.junit.runner.RunWith; @@ -78,6 +80,14 @@ public class FeignPageableEncodingTests { 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(); + if (optionalOrder.isPresent()) { + Sort.Order order = optionalOrder.get(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.ASC); + assertThat(order.getProperty()).isEqualTo("sortProperty"); + } } 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 new file mode 100644 index 00000000..c82ab731 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SortJacksonModuleTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2020 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.io.IOException; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Sort; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * @author Can Bezmen + */ +@ExtendWith(MockitoExtension.class) +class SortJacksonModuleTests { + + @Spy + private ObjectMapper objectMapper; + + @BeforeEach + public void setup() { + objectMapper.registerModules(new PageJacksonModule()); + objectMapper.registerModule(new SortJacksonModule()); + } + + @Test + public void deserializePage() 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 + 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 serializePage() throws IOException { + // Given + Sort sort = Sort.by(Sort.Order.by("fieldName")); + // When + String result = objectMapper.writeValueAsString(sort); + // Then + assertThat(result, containsString("\"direction\":\"ASC\"")); + assertThat(result, containsString("\"property\":\"fieldName\"")); + } + +}