diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b44059e78..8de18274e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -17,7 +17,7 @@ given the ability to merge pull requests. ## Code of Conduct This project adheres to the Contributor Covenant [code of -conduct](https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report +conduct](https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/modules/ROOT/partials/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. ## Code Conventions and Housekeeping diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5b829a0c7..3e82a383e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,17 +2,12 @@ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" - target-branch: "3.1.x" # oldest OSS supported branch + target-branch: "4.1.x" # oldest OSS supported branch schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" - target-branch: "4.0.x" # oldest OSS supported branch - schedule: - interval: "weekly" - - package-ecosystem: "github-actions" - directory: "/" - target-branch: "4.1.x" + target-branch: "4.2.x" schedule: interval: "weekly" - package-ecosystem: "github-actions" @@ -24,18 +19,7 @@ updates: directory: / schedule: interval: daily - target-branch: 3.1.x - ignore: - # only upgrade patch versions for maintenance branch - - dependency-name: "*" - update-types: - - version-update:semver-major - - version-update:semver-minor - - package-ecosystem: maven - directory: / - schedule: - interval: daily - target-branch: 4.0.x + target-branch: 4.1.x ignore: # only upgrade patch versions for maintenance branch - dependency-name: "*" @@ -46,7 +30,7 @@ updates: directory: / schedule: interval: daily - target-branch: 4.1.x + target-branch: 4.2.x ignore: # only upgrade patch versions for maintenance branch - dependency-name: "*" @@ -78,3 +62,8 @@ updates: directory: /docs schedule: interval: weekly + - package-ecosystem: npm + target-branch: 4.2.x + directory: /docs + schedule: + interval: weekly diff --git a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc index 52ee890be..c17f72a01 100644 --- a/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-openfeign.adoc @@ -146,7 +146,25 @@ When it comes to the Apache HttpClient 5-backed Feign clients, it's enough to en You can customize the HTTP client used by providing a bean of either `org.apache.hc.client5.http.impl.classic.CloseableHttpClient` when using Apache HC5. You can further customise http clients by setting values in the `spring.cloud.openfeign.httpclient.xxx` properties. The ones prefixed just with `httpclient` will work for all the clients, the ones prefixed with `httpclient.hc5` to Apache HttpClient 5, the ones prefixed with `httpclient.okhttp` to OkHttpClient and the ones prefixed with `httpclient.http2` to Http2Client. You can find a full list of properties you can customise in the appendix. -If you can not configure Apache HttpClient 5 by using properties, there is an `HttpClientBuilderCustomizer` interface for programmatic configuration. +If you can not configure Apache HttpClient 5 by using properties, there is an `HttpClient5FeignConfiguration.HttpClientBuilderCustomizer` interface for programmatic configuration. + +TIP: Apache HTTP Components `5.4` have changed defaults in the HttpClient relating to HTTP/1.1 TLS upgrades. Most proxy servers handle upgrades without issue, however, you may encounter issues with Envoy or Istio. If you need to restore previous behaviour, you can use `HttpClient5FeignConfiguration.HttpClientBuilderCustomizer` to do it, as shown in the example below. + +[source,java,indent=0] +---- +@Configuration +public class FooConfiguration { + + @Bean + public HttpClient5FeignConfiguration.HttpClientBuilderCustomizer httpClientBuilder() { + return (httpClientBuilder) -> { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + requestConfigBuilder.setProtocolUpgradeEnabled(false); + httpClientBuilder.setDefaultRequestConfig(requestConfigBuilder.build()); + }; + } +} +---- TIP: Starting with Spring Cloud OpenFeign 4, the Feign Apache HttpClient 4 is no longer supported. We suggest using Apache HttpClient 5 instead. @@ -288,7 +306,12 @@ public class CustomConfiguration { } ---- -TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `spring.cloud.openfeign.client.decodeSlash` to `false`. +TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `spring.cloud.openfeign.client.decode-slash` to `false`. + + +TIP: By default, Feign clients do not remove trailing slash `/` characters from the request path. +You can change this behaviour, by setting the value of `spring.cloud.openfeign.client.remove-trailing-slash` to `true`. +Trailing slash removal from the request path is going to be made the default behaviour in the next major release. [[springencoder-configuration]] ==== `SpringEncoder` configuration diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 81cc1df18..b8b6781be 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -1,7 +1,7 @@ |=== |Name | Default | Description -|spring.cloud.compatibility-verifier.compatible-boot-versions | | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.4.x} +|spring.cloud.compatibility-verifier.compatible-boot-versions | `+++3.5.x+++` | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.5.x} |spring.cloud.compatibility-verifier.enabled | `+++false+++` | Enables creation of Spring Cloud compatibility verification. |spring.cloud.config.allow-override | `+++true+++` | Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true. |spring.cloud.config.initialize-on-context-refresh | `+++false+++` | Flag to initialize bootstrap configuration on context refresh event. Default false. @@ -58,6 +58,7 @@ |spring.cloud.loadbalancer.retry.retry-on-all-operations | `+++false+++` | Indicates retries should be attempted on operations other than `HttpMethod.GET`. |spring.cloud.loadbalancer.retry.retryable-exceptions | `+++{}+++` | A `Set` of `Throwable` classes that should trigger a retry. |spring.cloud.loadbalancer.retry.retryable-status-codes | `+++{}+++` | A `Set` of status codes that should trigger a retry. +|spring.cloud.loadbalancer.stats.include-path | `+++true+++` | Indicates whether the {@code path} should be added to {@code uri} tag in metrics. When {@link RestTemplate} is used to execute load-balanced requests with high cardinality paths, setting it to {@code false} is recommended. |spring.cloud.loadbalancer.stats.micrometer.enabled | `+++false+++` | Enables micrometer metrics for load-balanced requests. |spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie | `+++false+++` | Indicates whether a cookie with the newly selected instance should be added by LoadBalancer. |spring.cloud.loadbalancer.sticky-session.instance-id-cookie-name | `+++sc-lb-instance-id+++` | The name of the cookie holding the preferred instance id. @@ -73,6 +74,7 @@ |spring.cloud.openfeign.client.default-config | `+++default+++` | |spring.cloud.openfeign.client.default-to-properties | `+++true+++` | |spring.cloud.openfeign.client.refresh-enabled | `+++false+++` | Enables options value refresh capability for Feign. +|spring.cloud.openfeign.client.remove-trailing-slash | `+++false+++` | If {@code true}, trailing slashes at the end of request urls will be removed. |spring.cloud.openfeign.compression.request.content-encoding-types | | The list of content encodings (applicable encodings depend on the used client). |spring.cloud.openfeign.compression.request.enabled | `+++false+++` | Enables the request sent by Feign to be compressed. |spring.cloud.openfeign.compression.request.mime-types | `+++[text/xml, application/xml, application/json]+++` | The list of supported mime types. @@ -85,19 +87,19 @@ |spring.cloud.openfeign.httpclient.disable-ssl-validation | `+++false+++` | |spring.cloud.openfeign.httpclient.follow-redirects | `+++true+++` | |spring.cloud.openfeign.httpclient.hc5.connection-request-timeout | `+++3+++` | Default value for connection request timeout. -|spring.cloud.openfeign.httpclient.hc5.connection-request-timeout-unit | | Default value for connection request timeout unit. +|spring.cloud.openfeign.httpclient.hc5.connection-request-timeout-unit | `+++minutes+++` | Default value for connection request timeout unit. |spring.cloud.openfeign.httpclient.hc5.enabled | `+++true+++` | Enables the use of the Apache HTTP Client 5 by Feign. -|spring.cloud.openfeign.httpclient.hc5.pool-concurrency-policy | | Pool concurrency policies. -|spring.cloud.openfeign.httpclient.hc5.pool-reuse-policy | | Pool connection re-use policies. +|spring.cloud.openfeign.httpclient.hc5.pool-concurrency-policy | `+++strict+++` | Pool concurrency policies. +|spring.cloud.openfeign.httpclient.hc5.pool-reuse-policy | `+++fifo+++` | Pool connection re-use policies. |spring.cloud.openfeign.httpclient.hc5.socket-timeout | `+++5+++` | Default value for socket timeout. -|spring.cloud.openfeign.httpclient.hc5.socket-timeout-unit | | Default value for socket timeout unit. +|spring.cloud.openfeign.httpclient.hc5.socket-timeout-unit | `+++seconds+++` | Default value for socket timeout unit. |spring.cloud.openfeign.httpclient.http2.version | `+++HTTP_2+++` | Configure the protocols used by this client to communicate with remote servers. Uses {@link String} value of {@link HttpClient.Version}. |spring.cloud.openfeign.httpclient.max-connections | `+++200+++` | |spring.cloud.openfeign.httpclient.max-connections-per-route | `+++50+++` | |spring.cloud.openfeign.httpclient.ok-http.protocols | | Configure the protocols used by this client to communicate with remote servers. Uses {@link String} values of {@link Protocol}. |spring.cloud.openfeign.httpclient.ok-http.read-timeout | `+++60s+++` | {@link OkHttpClient} read timeout; defaults to 60 seconds. |spring.cloud.openfeign.httpclient.time-to-live | `+++900+++` | -|spring.cloud.openfeign.httpclient.time-to-live-unit | | +|spring.cloud.openfeign.httpclient.time-to-live-unit | `+++seconds+++` | |spring.cloud.openfeign.lazy-attributes-resolution | `+++false+++` | Switches @FeignClient attributes resolution mode to lazy. |spring.cloud.openfeign.micrometer.enabled | `+++true+++` | Enables Micrometer capabilities for Feign. |spring.cloud.openfeign.oauth2.clientRegistrationId | | Provides a clientId to be used with OAuth2. @@ -105,8 +107,8 @@ |spring.cloud.openfeign.okhttp.enabled | `+++false+++` | Enables the use of the OK HTTP Client by Feign. |spring.cloud.refresh.additional-property-sources-to-retain | | Additional property sources to retain during a refresh. Typically only system property sources are retained. This property allows property sources, such as property sources created by EnvironmentPostProcessors to be retained as well. |spring.cloud.refresh.enabled | `+++true+++` | Enables autoconfiguration for the refresh scope and associated features. -|spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional class names for beans to post process into refresh scope. -|spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of class names for beans to never be refreshed or rebound. +|spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional bean names or class names for beans to post process into refresh scope. +|spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of bean names or class names for beans to never be refreshed or rebound. |spring.cloud.refresh.on-restart.enabled | `+++true+++` | Enable refreshing context on start. |spring.cloud.service-registry.auto-registration.enabled | `+++true+++` | Whether service auto-registration is enabled. Defaults to true. |spring.cloud.service-registry.auto-registration.fail-fast | `+++false+++` | Whether startup fails if there is no AutoServiceRegistration. Defaults to false. diff --git a/docs/pom.xml b/docs/pom.xml index f3068e824..6b9d3c1be 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,7 @@ org.springframework.cloud spring-cloud-openfeign - 4.1.5-SNAPSHOT + 4.3.0-SNAPSHOT .. diff --git a/pom.xml b/pom.xml index df793dfd5..c8d2c046c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,14 +4,14 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-openfeign - 4.1.5-SNAPSHOT + 4.3.0-SNAPSHOT pom Spring Cloud OpenFeign Spring Cloud OpenFeign org.springframework.cloud spring-cloud-build - 4.1.6-SNAPSHOT + 4.3.0-SNAPSHOT @@ -25,8 +25,8 @@ ${basedir} - 2.17.3 - 4.1.6-SNAPSHOT + 2.18.2 + 4.3.0-SNAPSHOT 2.10 @@ -78,6 +78,7 @@ mozilla/public-suffix-list.txt + mockito-extensions/org.mockito.plugins.MockResolver diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index cd726f90d..e30886c63 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-openfeign - 4.1.5-SNAPSHOT + 4.3.0-SNAPSHOT .. @@ -191,7 +191,7 @@ commons-io commons-io - 2.16.1 + 2.18.0 test diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java index bec1b653a..c6c26c017 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 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. @@ -61,6 +61,11 @@ public class FeignClientProperties { */ private boolean decodeSlash = true; + /** + * If {@code true}, trailing slashes at the end of request urls will be removed. + */ + private boolean removeTrailingSlash; + public boolean isDefaultToProperties() { return defaultToProperties; } @@ -93,6 +98,14 @@ public void setDecodeSlash(boolean decodeSlash) { this.decodeSlash = decodeSlash; } + public boolean isRemoveTrailingSlash() { + return removeTrailingSlash; + } + + public void setRemoveTrailingSlash(boolean removeTrailingSlash) { + this.removeTrailingSlash = removeTrailingSlash; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -103,12 +116,13 @@ public boolean equals(Object o) { } FeignClientProperties that = (FeignClientProperties) o; return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig) - && Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash); + && Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash) + && Objects.equals(removeTrailingSlash, that.removeTrailingSlash); } @Override public int hashCode() { - return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash); + return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash, removeTrailingSlash); } /** 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 3372a0c55..23dfcdfa0 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 @@ -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. @@ -145,8 +145,7 @@ public QueryMapEncoder feignQueryMapEncoderPageable() { @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { - boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash(); - return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash); + return new SpringMvcContract(parameterProcessors, feignConversionService, feignClientProperties); } @Bean diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java index 262c2c5e7..00fe8f195 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.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. @@ -67,6 +67,7 @@ * @author Marcin Grzejszczak * @author Olga Maciaszek-Sharma * @author Jasbir Singh + * @author Jinho Lee */ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { @@ -117,6 +118,9 @@ static String getUrl(String url) { if (!url.contains("://")) { url = "http://" + url; } + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } try { new URL(url); } @@ -321,11 +325,7 @@ private void validate(Map attributes) { validateFallbackFactory(annotation.getClass("fallbackFactory")); } - /* for testing */ String getName(Map attributes) { - return getName(null, attributes); - } - - String getName(ConfigurableBeanFactory beanFactory, Map attributes) { + String getName(Map attributes) { String name = (String) attributes.get("serviceId"); if (!StringUtils.hasText(name)) { name = (String) attributes.get("name"); @@ -333,7 +333,7 @@ String getName(ConfigurableBeanFactory beanFactory, Map attribut if (!StringUtils.hasText(name)) { name = (String) attributes.get("value"); } - name = resolve(beanFactory, name); + name = resolve(null, name); return getName(name); } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java index 918f0438a..bfdedd7d9 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java @@ -26,6 +26,8 @@ * * @author Aram Peres * @see feign.QueryMap + * @see feign.QueryMapEncoder + * @see org.springframework.cloud.openfeign.FeignClientsConfiguration * @see org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor */ @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java index 7744bf5bd..0728d90e0 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignBlockingLoadBalancerClient.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. @@ -71,7 +71,7 @@ public class FeignBlockingLoadBalancerClient implements Client { * @deprecated in favour of * {@link FeignBlockingLoadBalancerClient#FeignBlockingLoadBalancerClient(Client, LoadBalancerClient, LoadBalancerClientFactory, List)} */ - @Deprecated + @Deprecated(forRemoval = true) public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) { this.delegate = delegate; @@ -84,7 +84,7 @@ public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadB * @deprecated in favour of * {@link FeignBlockingLoadBalancerClient#FeignBlockingLoadBalancerClient(Client, LoadBalancerClient, LoadBalancerClientFactory, List)} */ - @Deprecated + @Deprecated(forRemoval = true) public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) { this.delegate = delegate; diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java index 77414bdf4..0b17f7e46 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/RetryableFeignBlockingLoadBalancerClient.java @@ -90,7 +90,7 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client { * @deprecated in favour of * {@link RetryableFeignBlockingLoadBalancerClient#RetryableFeignBlockingLoadBalancerClient(Client, LoadBalancerClient, LoadBalancedRetryFactory, LoadBalancerClientFactory, List)} */ - @Deprecated + @Deprecated(forRemoval = true) public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) { @@ -105,7 +105,7 @@ public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerCli * @deprecated in favour of * {@link RetryableFeignBlockingLoadBalancerClient#RetryableFeignBlockingLoadBalancerClient(Client, LoadBalancerClient, LoadBalancedRetryFactory, LoadBalancerClientFactory, List)} */ - @Deprecated + @Deprecated(forRemoval = true) public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) { this.delegate = delegate; @@ -257,6 +257,12 @@ public URI getURI() { return URI.create(request.url()); } + @Override + public Map getAttributes() { + Map attributes = new HashMap<>(request.requestTemplate().queries()); + return attributes; + } + @Override public HttpHeaders getHeaders() { Map> headers = new HashMap<>(); 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/main/java/org/springframework/cloud/openfeign/support/SpringDecoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringDecoder.java index b1219ec01..1067f2ab6 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringDecoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringDecoder.java @@ -87,7 +87,13 @@ public HttpStatusCode getStatusCode() { return HttpStatusCode.valueOf(response.status()); } - @Override + /** + * This method used to override a method from ClientHttpResponse interface but was + * removed in Spring Framework 6.2, so we should remove it as well. + * @deprecated in favour of + * {@link SpringDecoder.FeignResponseAdapter#getStatusCode()} + */ + @Deprecated(forRemoval = true) public int getRawStatusCode() { return response.status(); } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java index 9d9d7a8c1..6dbbdd1c1 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java @@ -41,6 +41,7 @@ import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.CollectionFormat; +import org.springframework.cloud.openfeign.FeignClientProperties; import org.springframework.cloud.openfeign.SpringQueryMap; import org.springframework.cloud.openfeign.annotation.CookieValueParameterProcessor; import org.springframework.cloud.openfeign.annotation.MatrixVariableParameterProcessor; @@ -115,6 +116,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource private final boolean decodeSlash; + private final boolean removeTrailingSlash; + public SpringMvcContract() { this(Collections.emptyList()); } @@ -128,8 +131,36 @@ public SpringMvcContract(List annotatedParameterPro this(annotatedParameterProcessors, conversionService, true); } + /** + * Creates a {@link SpringMvcContract} based on annotatedParameterProcessors, + * conversionService and decodeSlash value. + * @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor} + * objects used to resolve parameters + * @param conversionService {@link ConversionService} used for type conversion + * @param decodeSlash indicates whether slashes should be decoded + * @deprecated in favour of + * {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)} + */ + @Deprecated(forRemoval = true) public SpringMvcContract(List annotatedParameterProcessors, ConversionService conversionService, boolean decodeSlash) { + this(annotatedParameterProcessors, conversionService, decodeSlash, false); + } + + /** + * Creates a {@link SpringMvcContract} based on annotatedParameterProcessors, + * conversionService and decodeSlash value. + * @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor} + * objects used to resolve parameters + * @param conversionService {@link ConversionService} used for type conversion + * @param decodeSlash indicates whether slashes should be decoded + * @param removeTrailingSlash indicates whether trailing slashes should be removed + * @deprecated in favour of + * {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)} + */ + @Deprecated(forRemoval = true) + public SpringMvcContract(List annotatedParameterProcessors, + ConversionService conversionService, boolean decodeSlash, boolean removeTrailingSlash) { Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null."); Assert.notNull(conversionService, "ConversionService can not be null."); @@ -140,6 +171,14 @@ public SpringMvcContract(List annotatedParameterPro this.conversionService = conversionService; convertingExpanderFactory = new ConvertingExpanderFactory(conversionService); this.decodeSlash = decodeSlash; + this.removeTrailingSlash = removeTrailingSlash; + } + + public SpringMvcContract(List annotatedParameterProcessors, + ConversionService conversionService, FeignClientProperties feignClientProperties) { + this(annotatedParameterProcessors, conversionService, + feignClientProperties == null || feignClientProperties.isDecodeSlash(), + feignClientProperties != null && feignClientProperties.isRemoveTrailingSlash()); } private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) { @@ -229,6 +268,9 @@ protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodA if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) { pathValue = "/" + pathValue; } + if (removeTrailingSlash && pathValue.endsWith("/")) { + pathValue = pathValue.substring(0, pathValue.length() - 1); + } data.template().uri(pathValue, true); if (data.template().decodeSlash() != decodeSlash) { data.template().decodeSlash(decodeSlash); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java index 4f876c4da..67a9e85bd 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java @@ -131,7 +131,7 @@ void forType_allFieldsSetOnBuilder() { assertFactoryBeanField(builder, "contextId", "TestContext"); // and: - assertFactoryBeanField(builder, "url", "http://Url/"); + assertFactoryBeanField(builder, "url", "http://Url"); assertFactoryBeanField(builder, "path", "/Path"); assertFactoryBeanField(builder, "dismiss404", true); @@ -155,7 +155,7 @@ void forType_clientFactoryBeanProvided() { assertFactoryBeanField(builder, "contextId", "TestContext"); // and: - assertFactoryBeanField(builder, "url", "http://Url/"); + assertFactoryBeanField(builder, "url", "http://Url"); assertFactoryBeanField(builder, "path", "/Path"); assertFactoryBeanField(builder, "dismiss404", true); List additionalCustomizers = getFactoryBeanField(builder, "additionalCustomizers"); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java index 07f5a25b0..5459dd662 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.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. @@ -89,6 +89,12 @@ private String testGetName(String name) { return registrar.getName(Collections.singletonMap("name", name)); } + @Test + void testRemoveTrailingSlashFromUrl() { + String url = FeignClientsRegistrar.getUrl("http://localhost/"); + assertThat(url).isEqualTo("http://localhost"); + } + @Test void testFallback() { assertThatExceptionOfType(IllegalArgumentException.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 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")); + } + } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java index 02219e2ce..55a3b97d7 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test; import org.springframework.cloud.openfeign.CollectionFormat; +import org.springframework.cloud.openfeign.FeignClientProperties; import org.springframework.cloud.openfeign.SpringQueryMap; import org.springframework.core.convert.ConversionService; import org.springframework.data.domain.Page; @@ -189,8 +190,23 @@ void testProcessAnnotations_SimpleNoPath() throws Exception { } @Test - void testProcessAnnotations_SimplePathIsOnlyASlash() throws Exception { - Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPath", String.class); + void testProcessAnnotations_SimplePathIsOnlyASlashWithParam() throws Exception { + Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPathWithParam", String.class); + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/?id=" + "{id}"); + assertThat(data.template().method()).isEqualTo("GET"); + assertThat(data.template().headers().get("Accept").iterator().next()) + .isEqualTo(MediaType.APPLICATION_JSON_VALUE); + } + + @Test + void testProcessAnnotations_SimplePathIsOnlyASlashWithParamWithTrailingSlashRemoval() throws Exception { + FeignClientProperties properties = new FeignClientProperties(); + properties.setRemoveTrailingSlash(true); + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), properties); + Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPathWithParam", String.class); + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); assertThat(data.template().url()).isEqualTo("/?id=" + "{id}"); @@ -284,6 +300,48 @@ void testProcessAnnotations_SimplePostMapping() throws Exception { } + @Test + void testProcessAnnotations_SimplePathIsOnlyASlashWithTrailingSlashRemoval() throws Exception { + FeignClientProperties properties = new FeignClientProperties(); + properties.setRemoveTrailingSlash(true); + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), properties); + Method method = TestTemplate_Simple.class.getDeclaredMethod("getSlashPath"); + + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/"); + assertThat(data.template().method()).isEqualTo("GET"); + assertThat(data.template().headers().get("Accept").iterator().next()) + .isEqualTo(MediaType.APPLICATION_JSON_VALUE); + } + + @Test + void testProcessAnnotations_SimplePathHasTrailingSlash() throws Exception { + Method method = TestTemplate_Simple.class.getDeclaredMethod("getTrailingSlash"); + + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/test1/test2/"); + assertThat(data.template().method()).isEqualTo("GET"); + assertThat(data.template().headers().get("Accept").iterator().next()) + .isEqualTo(MediaType.APPLICATION_JSON_VALUE); + } + + @Test + void testProcessAnnotations_SimplePathHasTrailingSlashWithTrailingSlashRemoval() throws Exception { + FeignClientProperties properties = new FeignClientProperties(); + properties.setRemoveTrailingSlash(true); + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), properties); + Method method = TestTemplate_Simple.class.getDeclaredMethod("getTrailingSlash"); + + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/test1/test2"); + assertThat(data.template().method()).isEqualTo("GET"); + assertThat(data.template().headers().get("Accept").iterator().next()) + .isEqualTo(MediaType.APPLICATION_JSON_VALUE); + } + @Test void testProcessAnnotationsOnMethod_Advanced() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest", String.class, String.class, @@ -738,7 +796,13 @@ public interface TestTemplate_Simple { TestObject postMappingTest(@RequestBody TestObject object); @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - ResponseEntity getSlashPath(@RequestParam("id") String id); + ResponseEntity getSlashPathWithParam(@RequestParam("id") String id); + + @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity getSlashPath(); + + @GetMapping(value = "test1/test2/", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity getTrailingSlash(); @GetMapping(path = "test", produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity getTestNoLeadingSlash(@RequestParam("name") String name); diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index c10f36953..99e80bf80 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -6,11 +6,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.1.6-SNAPSHOT + 4.3.0-SNAPSHOT spring-cloud-openfeign-dependencies - 4.1.5-SNAPSHOT + 4.3.0-SNAPSHOT pom spring-cloud-openfeign-dependencies Spring Cloud OpenFeign Dependencies diff --git a/spring-cloud-starter-openfeign/pom.xml b/spring-cloud-starter-openfeign/pom.xml index abcbadaa0..b997aac42 100644 --- a/spring-cloud-starter-openfeign/pom.xml +++ b/spring-cloud-starter-openfeign/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-openfeign - 4.1.5-SNAPSHOT + 4.3.0-SNAPSHOT ..