From a6703ac5c9628cf4bd8d0a1558e6c215d69b40ee Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Mon, 26 Aug 2024 09:36:18 -0500 Subject: [PATCH 1/5] Add QUERY HTTP method --- .../MockHttpServletRequestBuilderTests.java | 15 +-- .../org/springframework/http/HttpMethod.java | 25 +++-- .../springframework/http/RequestEntity.java | 20 ++++ ...ttpComponentsClientHttpRequestFactory.java | 4 + .../web/bind/annotation/DeleteMapping.java | 1 + .../web/bind/annotation/GetMapping.java | 1 + .../web/bind/annotation/PatchMapping.java | 1 + .../web/bind/annotation/PostMapping.java | 1 + .../web/bind/annotation/PutMapping.java | 1 + .../web/bind/annotation/QueryMapping.java | 98 +++++++++++++++++++ .../web/bind/annotation/RequestMapping.java | 4 +- .../web/bind/annotation/RequestMethod.java | 6 +- .../web/client/DefaultRestClient.java | 5 + .../web/client/RestClient.java | 6 ++ .../web/client/RestOperations.java | 72 ++++++++++++++ .../web/client/RestTemplate.java | 62 ++++++++++++ .../web/cors/CorsConfiguration.java | 4 +- .../web/filter/ShallowEtagHeaderFilter.java | 2 +- .../adapter/DefaultServerWebExchange.java | 2 +- .../web/client/RestOperationsExtensions.kt | 82 ++++++++++++++++ .../springframework/http/HttpMethodTests.java | 4 +- .../web/client/RestTemplateTests.java | 77 +++++++++++++++ .../web/cors/CorsConfigurationTests.java | 10 +- .../web/cors/DefaultCorsProcessorTests.java | 2 +- .../reactive/DefaultCorsProcessorTests.java | 2 +- .../function/client/DefaultWebClient.java | 5 + .../reactive/function/client/WebClient.java | 6 ++ .../server/DefaultServerResponseBuilder.java | 2 +- .../function/server/RequestPredicates.java | 12 +++ .../server/ResourceHandlerFunction.java | 8 +- .../server/RouterFunctionBuilder.java | 24 +++++ .../function/server/RouterFunctions.java | 52 ++++++++++ .../reactive/resource/ResourceWebHandler.java | 2 +- .../RequestMethodsRequestCondition.java | 6 ++ .../RequestMappingInfoHandlerMapping.java | 2 +- .../ResponseEntityResultHandler.java | 2 +- .../server/ResourceHandlerFunctionTests.java | 4 +- .../GlobalCorsConfigIntegrationTests.java | 2 +- .../web/servlet/DispatcherServlet.java | 3 +- .../function/AbstractServerResponse.java | 2 +- .../servlet/function/RequestPredicates.java | 12 +++ .../function/ResourceHandlerFunction.java | 7 +- .../function/RouterFunctionBuilder.java | 24 +++++ .../web/servlet/function/RouterFunctions.java | 51 ++++++++++ .../RequestMethodsRequestCondition.java | 6 ++ .../RequestMappingInfoHandlerMapping.java | 2 +- .../annotation/HttpEntityMethodProcessor.java | 4 +- .../resource/ResourceHttpRequestHandler.java | 2 +- .../web/servlet/config/MvcNamespaceTests.java | 4 +- .../ResourceHandlerFunctionTests.java | 4 +- .../handler/HandlerMethodMappingTests.java | 2 +- ...RequestMappingInfoHandlerMappingTests.java | 8 +- .../ResourceHttpRequestHandlerTests.java | 2 +- .../support/WebContentGeneratorTests.java | 4 +- 54 files changed, 716 insertions(+), 55 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java index 6b0dd46fcdb5..0fd4ba20358b 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java @@ -53,6 +53,7 @@ import static org.assertj.core.api.Assertions.entry; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.QUERY; /** * Tests for building a {@link MockHttpServletRequest} with @@ -395,13 +396,15 @@ void requestParameterFromRequestBodyFormData() { String contentType = "application/x-www-form-urlencoded;charset=UTF-8"; String body = "name+1=value+1&name+2=value+A&name+2=value+B&name+3"; - MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST).uri("/foo") - .contentType(contentType).content(body.getBytes(UTF_8)) - .buildRequest(this.servletContext); + for (HttpMethod method : List.of(POST, QUERY)) { + MockHttpServletRequest request = new MockHttpServletRequestBuilder(method).uri("/foo") + .contentType(contentType).content(body.getBytes(UTF_8)) + .buildRequest(this.servletContext); - assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1"); - assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B"); - assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null); + assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1"); + assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B"); + assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null); + } } @Test diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index 12f77ffe4bc6..14bccb2c1d6b 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -36,25 +36,25 @@ public final class HttpMethod implements Comparable, Serializable { /** * The HTTP method {@code GET}. - * @see HTTP 1.1, section 9.3 + * @see HTTP Semantics, section 9.3.1 */ public static final HttpMethod GET = new HttpMethod("GET"); /** * The HTTP method {@code HEAD}. - * @see HTTP 1.1, section 9.4 + * @see HTTP Semantics, section 9.3.2 */ public static final HttpMethod HEAD = new HttpMethod("HEAD"); /** * The HTTP method {@code POST}. - * @see HTTP 1.1, section 9.5 + * @see HTTP Semantics, section 9.3.3 */ public static final HttpMethod POST = new HttpMethod("POST"); /** * The HTTP method {@code PUT}. - * @see HTTP 1.1, section 9.6 + * @see HTTP Semantics, section 9.3.4 */ public static final HttpMethod PUT = new HttpMethod("PUT"); @@ -66,23 +66,29 @@ public final class HttpMethod implements Comparable, Serializable { /** * The HTTP method {@code DELETE}. - * @see HTTP 1.1, section 9.7 + * @see HTTP Semantics, section 9.3.5 */ public static final HttpMethod DELETE = new HttpMethod("DELETE"); /** * The HTTP method {@code OPTIONS}. - * @see HTTP 1.1, section 9.2 + * @see HTTP Semantics, section 9.3.7 */ public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS"); /** * The HTTP method {@code TRACE}. - * @see HTTP 1.1, section 9.8 + * @see HTTP Semantics, section 9.3.8 */ public static final HttpMethod TRACE = new HttpMethod("TRACE"); - private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE }; + /** + * The HTTP method {@code QUERY}. + * @see IETF Draft + */ + public static final HttpMethod QUERY = new HttpMethod("QUERY"); + + private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY }; private final String name; @@ -96,7 +102,7 @@ private HttpMethod(String name) { * Returns an array containing the standard HTTP methods. Specifically, * this method returns an array containing {@link #GET}, {@link #HEAD}, * {@link #POST}, {@link #PUT}, {@link #PATCH}, {@link #DELETE}, - * {@link #OPTIONS}, and {@link #TRACE}. + * {@link #OPTIONS}, {@link #TRACE}, and {@link #QUERY}. * *

Note that the returned value does not include any HTTP methods defined * in WebDav. @@ -123,6 +129,7 @@ public static HttpMethod valueOf(String method) { case "DELETE" -> DELETE; case "OPTIONS" -> OPTIONS; case "TRACE" -> TRACE; + case "QUERY" -> QUERY; default -> new HttpMethod(method); }; } diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index d387735f8b7a..6e2435f27f70 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -335,6 +335,26 @@ public static BodyBuilder post(String uriTemplate, Object... uriVariables) { return method(HttpMethod.POST, uriTemplate, uriVariables); } + /** + * Create an HTTP QUERY builder with the given url. + * @param url the URL + * @return the created builder + */ + public static BodyBuilder query(URI url) { + return method(HttpMethod.QUERY, url); + } + + /** + * Create an HTTP QUERY builder with the given string base uri template. + * @param uriTemplate the uri template to use + * @param uriVariables variables to expand the URI template with + * @return the created builder + * @since 6.2 + */ + public static BodyBuilder query(String uriTemplate, Object... uriVariables) { + return method(HttpMethod.QUERY, uriTemplate, uriVariables); + } + /** * Create an HTTP PUT builder with the given url. * @param url the URL diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index d5d73e8f8910..476f1b727485 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -32,6 +32,7 @@ import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPut; import org.apache.hc.client5.http.classic.methods.HttpTrace; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.client5.http.config.Configurable; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.HttpClients; @@ -307,6 +308,9 @@ else if (HttpMethod.OPTIONS.equals(httpMethod)) { else if (HttpMethod.TRACE.equals(httpMethod)) { return new HttpTrace(uri); } + else if (HttpMethod.QUERY.equals(httpMethod)) { + return new HttpUriRequestBase(HttpMethod.QUERY.name(), uri); + } throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java index 4681a719a9ba..fbdea9bf4f4f 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java @@ -44,6 +44,7 @@ * @see PostMapping * @see PutMapping * @see PatchMapping + * @see QueryMapping * @see RequestMapping */ @Target(ElementType.METHOD) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java index 22092f77fc51..43e3fbb0bf0a 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java @@ -44,6 +44,7 @@ * @see PutMapping * @see DeleteMapping * @see PatchMapping + * @see QueryMapping * @see RequestMapping */ @Target(ElementType.METHOD) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java index c11f39e4b0a7..140156fed309 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java @@ -44,6 +44,7 @@ * @see PostMapping * @see PutMapping * @see DeleteMapping + * @see QueryMapping * @see RequestMapping */ @Target(ElementType.METHOD) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java index 18a0b47db553..75df360a9c4c 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java @@ -44,6 +44,7 @@ * @see PutMapping * @see DeleteMapping * @see PatchMapping + * @see QueryMapping * @see RequestMapping */ @Target(ElementType.METHOD) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java index 8e8cb005d0a4..d5c5f89355c5 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java @@ -44,6 +44,7 @@ * @see PostMapping * @see DeleteMapping * @see PatchMapping + * @see QueryMapping * @see RequestMapping */ @Target(ElementType.METHOD) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java new file mode 100644 index 000000000000..20ee9f9b3579 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-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. + * 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.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code QUERY} requests onto specific handler + * methods. + * + *

Specifically, {@code @QueryMapping} is a composed annotation that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.QUERY)}. + * + *

NOTE: This annotation cannot be used in conjunction with + * other {@code @RequestMapping} annotations that are declared on the same method. + * If multiple {@code @RequestMapping} annotations are detected on the same method, + * a warning will be logged, and only the first mapping will be used. This applies + * to {@code @RequestMapping} as well as composed {@code @RequestMapping} annotations + * such as {@code @GetMapping}, {@code @PutMapping}, etc. + * + * @author Mario Ruiz + * @since 6.2 + * @see GetMapping + * @see PutMapping + * @see PostMapping + * @see DeleteMapping + * @see PatchMapping + * @see RequestMapping + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.QUERY) +public @interface QueryMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 0bff16d474fb..1736b21ed172 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -51,8 +51,8 @@ * at the method level. In most cases, at the method level applications will * prefer to use one of the HTTP method specific variants * {@link GetMapping @GetMapping}, {@link PostMapping @PostMapping}, - * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or - * {@link PatchMapping @PatchMapping}. + * {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, + * {@link PatchMapping @PatchMapping}, or {@link QueryMapping}. * *

NOTE: This annotation cannot be used in conjunction with * other {@code @RequestMapping} annotations that are declared on the same element diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java index c7564093cdd9..eb1bc3816ef1 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java @@ -25,7 +25,7 @@ * {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation. * *

Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet} - * supports GET, HEAD, POST, PUT, PATCH, and DELETE only. DispatcherServlet will + * supports GET, QUERY, HEAD, POST, PUT, PATCH, and DELETE only. DispatcherServlet will * process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly * told to dispatch those request types as well: Check out the "dispatchOptionsRequest" * and "dispatchTraceRequest" properties, switching them to "true" if necessary. @@ -38,7 +38,7 @@ */ public enum RequestMethod { - GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY; /** @@ -60,6 +60,7 @@ public static RequestMethod resolve(String method) { case "DELETE" -> DELETE; case "OPTIONS" -> OPTIONS; case "TRACE" -> TRACE; + case "QUERY" -> QUERY; default -> null; }; } @@ -93,6 +94,7 @@ public HttpMethod asHttpMethod() { case DELETE -> HttpMethod.DELETE; case OPTIONS -> HttpMethod.OPTIONS; case TRACE -> HttpMethod.TRACE; + case QUERY -> HttpMethod.QUERY; }; } diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java index 9bf267c4214e..7a747a392e45 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java @@ -178,6 +178,11 @@ public RequestHeadersUriSpec options() { return methodInternal(HttpMethod.OPTIONS); } + @Override + public RequestHeadersUriSpec query() { + return methodInternal(HttpMethod.QUERY); + } + @Override public RequestBodyUriSpec method(HttpMethod method) { Assert.notNull(method, "HttpMethod must not be null"); diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClient.java b/spring-web/src/main/java/org/springframework/web/client/RestClient.java index 8daef751c67d..7be44e2a5306 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestClient.java @@ -119,6 +119,12 @@ public interface RestClient { */ RequestHeadersUriSpec options(); + /** + * Start building an HTTP QUERY request. + * @return a spec for specifying the target URL + */ + RequestHeadersUriSpec query(); + /** * Start building a request for the given {@code HttpMethod}. * @return a spec for specifying the target URL diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index 440fad582a5e..e4a649c4970d 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -485,6 +485,78 @@ T patchForObject(URI url, @Nullable Object request, Class responseType) */ Set optionsForAllow(URI url) throws RestClientException; + // QUERY + + /** + * Retrieve a representation by doing a QUERY on the specified URL. + * The response (if any) is converted and returned. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + */ + @Nullable + T queryForObject(String url, @Nullable Object request, Class responseType, Object... uriVariables) throws RestClientException; + + /** + * Retrieve a representation by doing a QUERY on the URI template. + * The response (if any) is converted and returned. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the map containing variables for the URI template + * @return the converted object + */ + @Nullable + T queryForObject(String url, @Nullable Object request, Class responseType, Map uriVariables) throws RestClientException; + + /** + * Retrieve a representation by doing a QUERY on the URL. + * The response (if any) is converted and returned. + * @param url the URL + * @param responseType the type of the return value + * @return the converted object + */ + @Nullable + T queryForObject(URI url, @Nullable Object request, Class responseType) throws RestClientException; + + /** + * Retrieve an entity by doing a QUERY on the specified URL. + * The response is converted and stored in a {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the entity + * @since 6.2 + */ + ResponseEntity queryForEntity(String url, @Nullable Object request, Class responseType, Object... uriVariables) + throws RestClientException; + + /** + * Retrieve a representation by doing a QUERY on the URI template. + * The response is converted and stored in a {@link ResponseEntity}. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the map containing variables for the URI template + * @return the converted object + * @since 6.2 + */ + ResponseEntity queryForEntity(String url, @Nullable Object request, Class responseType, Map uriVariables) + throws RestClientException; + + /** + * Retrieve a representation by doing a GET on the URL. + * The response is converted and stored in a {@link ResponseEntity}. + * @param url the URL + * @param responseType the type of the return value + * @return the converted object + * @since 6.2 + */ + ResponseEntity queryForEntity(URI url, @Nullable Object request, Class responseType) throws RestClientException; + // exchange diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index e9784a5e4348..a25169d92f26 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -374,6 +374,7 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { /** * Return the configured {@link ObservationRegistry}. + * * @since 6.1 */ public ObservationRegistry getObservationRegistry() { @@ -670,6 +671,67 @@ public Set optionsForAllow(URI url) throws RestClientException { return (headers != null ? headers.getAllow() : Collections.emptySet()); } + // QUERY + + @Override + @Nullable + public T queryForObject(String url, @Nullable Object request, Class responseType, + Object... uriVariables) throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); + return execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables); + } + + @Override + @Nullable + public T queryForObject(String url, @Nullable Object request, Class responseType, + Map uriVariables) throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); + return execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables); + } + + @Override + @Nullable + public T queryForObject(URI url, @Nullable Object request, Class responseType) + throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor<>(responseType, getMessageConverters()); + return execute(url, HttpMethod.QUERY, requestCallback, responseExtractor); + } + + @Override + public ResponseEntity queryForEntity(String url, @Nullable Object request, + Class responseType, Object... uriVariables) throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return nonNull(execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables)); + } + + @Override + public ResponseEntity queryForEntity(String url, @Nullable Object request, + Class responseType, Map uriVariables) throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return nonNull(execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables)); + } + + @Override + public ResponseEntity queryForEntity(URI url, @Nullable Object request, Class responseType) + throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return nonNull(execute(url, HttpMethod.QUERY, requestCallback, responseExtractor)); + } // exchange diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 032c0de7ffdb..e54eb341403c 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -62,9 +62,9 @@ public class CorsConfiguration { private static final List DEFAULT_PERMIT_ALL = Collections.singletonList(ALL); - private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.HEAD); + private static final List DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); - private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), + private static final List DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index 7f830f9bdb4a..99c2cf1ee136 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -151,7 +151,7 @@ protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletRespo if (!response.isCommitted() && responseStatusCode >= 200 && responseStatusCode < 300 && - HttpMethod.GET.matches(request.getMethod())) { + (HttpMethod.GET.matches(request.getMethod()) || HttpMethod.QUERY.matches(request.getMethod()))) { String cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL); return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE)); diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index c17beb484b8b..1f27df439711 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -65,7 +65,7 @@ */ public class DefaultServerWebExchange implements ServerWebExchange { - private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD); + private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); private static final ResolvableType FORM_DATA_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class); diff --git a/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt b/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt index a6234cfb1402..c9d5125d4c01 100644 --- a/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt +++ b/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt @@ -234,6 +234,88 @@ inline fun RestOperations.postForEntity(url: String, request: Any? = inline fun RestOperations.postForEntity(url: URI, request: Any? = null): ResponseEntity = postForEntity(url, request, T::class.java) +/** + * Extension for [RestOperations.queryForObject] providing a `queryForObject(...)` + * variant leveraging Kotlin reified type parameters. Like the original Java method, this + * extension is subject to type erasure. Use [exchange] if you need to retain actual + * generic type arguments. + * + * @author Mario Ruiz + * @since 6.2 + */ +@Throws(RestClientException::class) +inline fun RestOperations.queryForObject(url: String, request: Any? = null, + vararg uriVariables: Any): T = + queryForObject(url, request, T::class.java, *uriVariables) as T + +/** + * Extension for [RestOperations.queryForObject] providing a `queryForObject(...)` + * variant leveraging Kotlin reified type parameters. Like the original Java method, this + * extension is subject to type erasure. Use [exchange] if you need to retain actual + * generic type arguments. + * + * @author Mario Ruiz + * @since 6.2 + */ +@Throws(RestClientException::class) +inline fun RestOperations.queryForObject(url: String, request: Any? = null, + uriVariables: Map): T = + queryForObject(url, request, T::class.java, uriVariables) as T + +/** + * Extension for [RestOperations.queryForObject] providing a `queryForObject(...)` + * variant leveraging Kotlin reified type parameters. Like the original Java method, this + * extension is subject to type erasure. Use [exchange] if you need to retain actual + * generic type arguments. + * + * @author Mario Ruiz + * @since 6.2 + */ +@Throws(RestClientException::class) +inline fun RestOperations.queryForObject(url: URI, request: Any? = null): T = + queryForObject(url, request, T::class.java) as T + +/** + * Extension for [RestOperations.queryForEntity] providing a `queryForEntity(...)` + * variant leveraging Kotlin reified type parameters. Like the original Java method, this + * extension is subject to type erasure. Use [exchange] if you need to retain actual + * generic type arguments. + * + * @author Mario Ruiz + * @since 6.2 + */ +@Throws(RestClientException::class) +inline fun RestOperations.queryForEntity(url: String, request: Any? = null, + vararg uriVariables: Any): ResponseEntity = + queryForEntity(url, request, T::class.java, *uriVariables) + +/** + * Extension for [RestOperations.queryForEntity] providing a `queryForEntity(...)` + * variant leveraging Kotlin reified type parameters. Like the original Java method, this + * extension is subject to type erasure. Use [exchange] if you need to retain actual + * generic type arguments. + * + * @author Mario Ruiz + * @since 6.2 + */ +@Throws(RestClientException::class) +inline fun RestOperations.queryForEntity(url: String, request: Any? = null, + uriVariables: Map): ResponseEntity = + queryForEntity(url, request, T::class.java, uriVariables) + +/** + * Extension for [RestOperations.queryForEntity] providing a `queryForEntity(...)` + * variant leveraging Kotlin reified type parameters. Like the original Java method, this + * extension is subject to type erasure. Use [exchange] if you need to retain actual + * generic type arguments. + * + * @author Mario Ruiz + * @since 6.2 + */ +@Throws(RestClientException::class) +inline fun RestOperations.queryForEntity(url: URI, request: Any? = null): ResponseEntity = + queryForEntity(url, request, T::class.java) + /** * Extension for [RestOperations.exchange] providing an `exchange(...)` * variant leveraging Kotlin reified type parameters. This extension is not subject to diff --git a/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java b/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java index 055149ea53e1..eb5aff2f6975 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpMethodTests.java @@ -44,12 +44,12 @@ void comparison() { void values() { HttpMethod[] values = HttpMethod.values(); assertThat(values).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT, - HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE); + HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE, HttpMethod.QUERY); // check defensive copy values[0] = HttpMethod.POST; assertThat(HttpMethod.values()).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT, - HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE); + HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE, HttpMethod.QUERY); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index cf7f6fadd44e..47a12a26041e 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -72,6 +72,7 @@ import static org.springframework.http.HttpMethod.PATCH; import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.PUT; +import static org.springframework.http.HttpMethod.QUERY; import static org.springframework.http.MediaType.parseMediaType; /** @@ -473,6 +474,82 @@ void postForEntityNull() throws Exception { verify(response).close(); } + @Test + void queryForObject() throws Exception { + mockTextPlainHttpMessageConverter(); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(QUERY, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + String expected = "42"; + mockResponseBody(expected, MediaType.TEXT_PLAIN); + + String result = template.queryForObject("https://example.com", "Hello World", String.class); + assertThat(result).as("Invalid QUERY result").isEqualTo(expected); + assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); + + verify(response).close(); + } + + @Test + void queryForEntity() throws Exception { + mockTextPlainHttpMessageConverter(); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(QUERY, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + String expected = "42"; + mockResponseBody(expected, MediaType.TEXT_PLAIN); + + ResponseEntity result = template.queryForEntity("https://example.com", "Hello World", String.class); + assertThat(result.getBody()).as("Invalid QUERY result").isEqualTo(expected); + assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN); + assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); + assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); + + verify(response).close(); + } + + @Test + void queryForObjectNull() throws Exception { + mockTextPlainHttpMessageConverter(); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(QUERY, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.TEXT_PLAIN); + responseHeaders.setContentLength(10); + given(response.getHeaders()).willReturn(responseHeaders); + given(response.getBody()).willReturn(InputStream.nullInputStream()); + given(converter.read(String.class, response)).willReturn(null); + + String result = template.queryForObject("https://example.com", null, String.class); + assertThat(result).as("Invalid QUERY result").isNull(); + assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0); + + verify(response).close(); + } + + @Test + void queryForEntityNull() throws Exception { + mockTextPlainHttpMessageConverter(); + HttpHeaders requestHeaders = new HttpHeaders(); + mockSentRequest(QUERY, "https://example.com", requestHeaders); + mockResponseStatus(HttpStatus.OK); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.TEXT_PLAIN); + responseHeaders.setContentLength(10); + given(response.getHeaders()).willReturn(responseHeaders); + given(response.getBody()).willReturn(InputStream.nullInputStream()); + given(converter.read(String.class, response)).willReturn(null); + + ResponseEntity result = template.queryForEntity("https://example.com", null, String.class); + assertThat(result.hasBody()).as("Invalid QUERY result").isFalse(); + assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN); + assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0); + assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); + + verify(response).close(); + } + @Test void put() throws Exception { mockTextPlainHttpMessageConverter(); diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 092781475f94..1f8fb86ea426 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -140,7 +140,7 @@ void combineWithDefaultPermitValues() { assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) - .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + .containsExactly(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = new CorsConfiguration().combine(config); @@ -148,7 +148,7 @@ void combineWithDefaultPermitValues() { assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) - .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + .containsExactly(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); assertThat(combinedConfig.getExposedHeaders()).isEmpty(); } @@ -394,7 +394,9 @@ void checkOriginPatternNotAllowed() { @Test void checkMethodAllowed() { CorsConfiguration config = new CorsConfiguration(); - assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.HEAD); + assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); + assertThat(config.checkHttpMethod(HttpMethod.QUERY)).containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); + config.addAllowedMethod("GET"); assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET); @@ -450,7 +452,7 @@ void changePermitDefaultValues() { assertThat(config.getAllowedOrigins()).containsExactly("*", "https://domain.com"); assertThat(config.getAllowedHeaders()).containsExactly("*", "header1"); - assertThat(config.getAllowedMethods()).containsExactly("GET", "HEAD", "POST", "PATCH"); + assertThat(config.getAllowedMethods()).containsExactly("GET", "QUERY", "HEAD", "POST", "PATCH"); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 301154fe015f..70c3dd11dc21 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -238,7 +238,7 @@ void preflightRequestMatchedAllowedMethod() throws Exception { this.processor.processRequest(this.conf, this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,HEAD"); + assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,QUERY,HEAD"); assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index fcb805129d94..5435c49f07c8 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -247,7 +247,7 @@ void preflightRequestMatchedAllowedMethod() { assertThat(response.getStatusCode()).isNull(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); - assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,HEAD"); + assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,QUERY,HEAD"); } @Test diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 01ea08c5098c..f8511701eec4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -171,6 +171,11 @@ public RequestHeadersUriSpec options() { return methodInternal(HttpMethod.OPTIONS); } + @Override + public RequestHeadersUriSpec query() { + return methodInternal(HttpMethod.QUERY); + } + @Override public RequestBodyUriSpec method(HttpMethod httpMethod) { return methodInternal(httpMethod); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index ffb454b76268..57b8b6b961b0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -121,6 +121,12 @@ public interface WebClient { */ RequestHeadersUriSpec options(); + /** + * Start building an HTTP QUERY request. + * @return a spec for specifying the target URL + */ + RequestHeadersUriSpec query(); + /** * Start building a request for the given {@code HttpMethod}. * @return a spec for specifying the target URL diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java index 9f0df3b736de..e9e1ad69fbfb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java @@ -300,7 +300,7 @@ public Mono render(String name, Map model) { */ abstract static class AbstractServerResponse implements ServerResponse { - private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD); + private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); private final HttpStatusCode statusCode; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index 157a360aa7a8..25ea9b46674a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -266,6 +266,18 @@ public static RequestPredicate OPTIONS(String pattern) { return method(HttpMethod.OPTIONS).and(path(pattern)); } + /** + * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code QUERY} + * and the given {@code pattern} matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is QUERY and if the given pattern + * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern + */ + public static RequestPredicate QUERY(String pattern) { + return method(HttpMethod.QUERY).and(path(pattern)); + } + /** * Return a {@code RequestPredicate} that matches if the request's path has the given extension. * @param extension the path extension to match against, ignoring case diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java index 2fa724917de2..ab1d3642c3c7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java @@ -43,7 +43,7 @@ class ResourceHandlerFunction implements HandlerFunction { private static final Set SUPPORTED_METHODS = - Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS); + Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS); private final Resource resource; @@ -66,6 +66,12 @@ public Mono handle(ServerRequest request) { .build() .map(response -> response); } + else if (HttpMethod.QUERY.equals(method)) { + return EntityResponse.fromObject(this.resource) + .headers(headers -> this.headersConsumer.accept(this.resource, headers)) + .build() + .map(response -> response); + } else if (HttpMethod.HEAD.equals(method)) { Resource headResource = new HeadMethodResource(this.resource); return EntityResponse.fromObject(headResource) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java index e8133b2f5cc0..76bd64dcb18d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java @@ -231,6 +231,30 @@ public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicat return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction); } + // QUERY + + @Override + public RouterFunctions.Builder QUERY(HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, HandlerFunction handlerFunction) { + return add(RequestPredicates.QUERY(pattern), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate, + HandlerFunction handlerFunction) { + + return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction); + } + // other @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index 661d1e8e6600..ba0bc526b966 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -696,6 +696,58 @@ public interface Builder { */ Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction handlerFunction); + /** + * Adds a route to the given handler function that handles HTTP {@code QUERY} requests. + * @param handlerFunction the handler function to handle all {@code QUERY} requests + * @return this builder + * @since 6.2 + */ + Builder QUERY(HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given pattern. + * @param pattern the pattern to match to + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code pattern} + * @return this builder + * @see org.springframework.web.util.pattern.PathPattern + */ + Builder QUERY(String pattern, HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given predicate. + * @param predicate predicate to match + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code predicate} + * @return this builder + * @since 6.2 + * @see RequestPredicates + */ + Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given pattern and predicate. + *

For instance, the following example routes QUERY requests for "/user" that contain JSON + * to the {@code addUser} method in {@code userController}: + *

+		 * RouterFunction<ServerResponse> route =
+		 *   RouterFunctions.route()
+		 *     .QUERY("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
+		 *     .build();
+		 * 
+ * @param pattern the pattern to match to + * @param predicate additional predicate to match + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code pattern} + * @return this builder + * @see org.springframework.web.util.pattern.PathPattern + */ + Builder QUERY(String pattern, RequestPredicate predicate, HandlerFunction handlerFunction); + + /** * Adds a route to the given handler function that handles all requests that match the * given predicate. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index 3f4721a5b02d..68140764d7ec 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -89,7 +89,7 @@ */ public class ResourceWebHandler implements WebHandler, InitializingBean { - private static final Set SUPPORTED_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD); + private static final Set SUPPORTED_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); private static final Log logger = LogFactory.getLog(ResourceWebHandler.class); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java index e8f0cd32d00d..0ed4d08805bd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java @@ -159,6 +159,9 @@ private RequestMethodsRequestCondition matchRequestMethod(HttpMethod httpMethod) if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) { return requestMethodConditionCache.get(HttpMethod.GET); } + if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.QUERY)) { + return requestMethodConditionCache.get(HttpMethod.QUERY); + } } return null; } @@ -186,6 +189,9 @@ else if (this.methods.size() == 1) { else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) { return 1; } + else if (this.methods.contains(RequestMethod.QUERY) && other.methods.contains(RequestMethod.HEAD)) { + return 1; + } } return 0; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 94d0cf9cfc37..6ef4a21a4cff 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -416,7 +416,7 @@ private static Set initAllowedHttpMethods(Set declaredMe } else { Set result = new LinkedHashSet<>(declaredMethods); - if (result.contains(HttpMethod.GET)) { + if (result.contains(HttpMethod.GET) || result.contains(HttpMethod.QUERY)) { result.add(HttpMethod.HEAD); } result.add(HttpMethod.OPTIONS); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index 3d91c62a7de7..c2b513b0d42b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -56,7 +56,7 @@ */ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler implements HandlerResultHandler { - private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD); + private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); /** diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java index 8d270c98a378..141f60d0a982 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java @@ -141,7 +141,7 @@ void options() { Mono responseMono = this.handlerFunction.handle(request); Mono result = responseMono.flatMap(response -> { assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS)); + assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.QUERY)); return response.writeTo(exchange, context); }); @@ -150,7 +150,7 @@ void options() { .expectComplete() .verify(); assertThat(mockResponse.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(mockResponse.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS)); + assertThat(mockResponse.getHeaders().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.QUERY)); StepVerifier.create(mockResponse.getBody()).expectComplete().verify(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java index de3fb9d8b638..2c5c64124112 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/GlobalCorsConfigIntegrationTests.java @@ -117,7 +117,7 @@ void preFlightRequestWithCorsEnabled(HttpServer httpServer) throws Exception { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getHeaders().getAccessControlAllowOrigin()).isEqualTo("*"); assertThat(entity.getHeaders().getAccessControlAllowMethods()) - .containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST); + .containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.POST); } @ParameterizedHttpServerTest diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 0c33b2b62b9b..d9b6b87a6aac 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -1073,7 +1073,8 @@ protected void doDispatch(HttpServletRequest request, HttpServletResponse respon // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); - if (isGet || HttpMethod.HEAD.matches(method)) { + boolean isQuery = HttpMethod.QUERY.matches(method); + if (isGet || isQuery || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java index 704ae1798a79..ff7281dc6d67 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java @@ -43,7 +43,7 @@ */ abstract class AbstractServerResponse extends ErrorHandlingServerResponse { - private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD); + private static final Set SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD); private final HttpStatusCode statusCode; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index aa71230ed33b..9fab0a2fa324 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -265,6 +265,18 @@ public static RequestPredicate OPTIONS(String pattern) { return method(HttpMethod.OPTIONS).and(path(pattern)); } + /** + * Return a {@code RequestPredicate} that matches if request's HTTP method is {@code QUERY} + * and the given {@code pattern} matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is QUERY and if the given pattern + * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern + */ + public static RequestPredicate QUERY(String pattern) { + return method(HttpMethod.QUERY).and(path(pattern)); + } + /** * Return a {@code RequestPredicate} that matches if the request's path has the given extension. * @param extension the path extension to match against, ignoring case diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java index 3ee4a3df6012..1df20298fdfd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ResourceHandlerFunction.java @@ -40,7 +40,7 @@ class ResourceHandlerFunction implements HandlerFunction { private static final Set SUPPORTED_METHODS = - Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS); + Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS); private final Resource resource; @@ -62,6 +62,11 @@ public ServerResponse handle(ServerRequest request) { .headers(headers -> this.headersConsumer.accept(this.resource, headers)) .build(); } + else if (HttpMethod.QUERY.equals(method)) { + return EntityResponse.fromObject(this.resource) + .headers(headers -> this.headersConsumer.accept(this.resource, headers)) + .build(); + } else if (HttpMethod.HEAD.equals(method)) { Resource headResource = new HeadMethodResource(this.resource); return EntityResponse.fromObject(headResource) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java index 6a5c4806b731..7ce41621ec81 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java @@ -229,6 +229,30 @@ public RouterFunctions.Builder OPTIONS(String pattern, RequestPredicate predicat return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction); } + // QUERY + + @Override + public RouterFunctions.Builder QUERY(HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction) { + return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, HandlerFunction handlerFunction) { + return add(RequestPredicates.QUERY(pattern), handlerFunction); + } + + @Override + public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate, + HandlerFunction handlerFunction) { + + return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction); + } + // other @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 4a10d7261278..1a6f5b37195e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java @@ -606,6 +606,57 @@ public interface Builder { */ Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction handlerFunction); + /** + * Adds a route to the given handler function that handles HTTP {@code QUERY} requests. + * @param handlerFunction the handler function to handle all {@code QUERY} requests + * @return this builder + * @since 5.3 + */ + Builder QUERY(HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given pattern. + * @param pattern the pattern to match to + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code pattern} + * @return this builder + * @see org.springframework.web.util.pattern.PathPattern + */ + Builder QUERY(String pattern, HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given predicate. + * @param predicate predicate to match + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code predicate} + * @return this builder + * @since 5.3 + * @see RequestPredicates + */ + Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction); + + /** + * Adds a route to the given handler function that handles all HTTP {@code QUERY} requests + * that match the given pattern and predicate. + *

For instance, the following example routes QUERY requests for "/user" that contain JSON + * to the {@code addUser} method in {@code userController}: + *

+		 * RouterFunction<ServerResponse> route =
+		 *   RouterFunctions.route()
+		 *     .QUERY("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
+		 *     .build();
+		 * 
+ * @param pattern the pattern to match to + * @param predicate additional predicate to match + * @param handlerFunction the handler function to handle all {@code QUERY} requests that + * match {@code pattern} + * @return this builder + * @see org.springframework.web.util.pattern.PathPattern + */ + Builder QUERY(String pattern, RequestPredicate predicate, HandlerFunction handlerFunction); + /** * Adds a route to the given handler function that handles all requests that match the * given predicate. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java index 7fd5b7837248..9f538ec2c2b8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java @@ -165,6 +165,9 @@ private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) { return requestMethodConditionCache.get(HttpMethod.GET.name()); } + if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.QUERY)) { + return requestMethodConditionCache.get(HttpMethod.QUERY.name()); + } } return null; } @@ -192,6 +195,9 @@ else if (this.methods.size() == 1) { else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) { return 1; } + else if (this.methods.contains(RequestMethod.QUERY) && other.methods.contains(RequestMethod.HEAD)) { + return 1; + } } return 0; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index b98af5617652..46dba7d42652 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -526,7 +526,7 @@ private static Set initAllowedHttpMethods(Set declaredMethod for (String method : declaredMethods) { HttpMethod httpMethod = HttpMethod.valueOf(method); result.add(httpMethod); - if (httpMethod == HttpMethod.GET) { + if (httpMethod == HttpMethod.GET || httpMethod == HttpMethod.QUERY) { result.add(HttpMethod.HEAD); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index fda6f3adbbda..170ecb993f7e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -243,7 +243,7 @@ else if (returnValue instanceof ProblemDetail detail) { outputMessage.getServletResponse().setStatus(returnStatus); if (returnStatus == 200) { HttpMethod method = inputMessage.getMethod(); - if ((HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method)) + if ((HttpMethod.GET.equals(method) || HttpMethod.QUERY.equals(method) || HttpMethod.HEAD.equals(method)) && isResourceNotModified(inputMessage, outputMessage)) { outputMessage.flush(); return; @@ -292,7 +292,7 @@ private boolean isResourceNotModified(ServletServerHttpRequest request, ServletS HttpHeaders responseHeaders = response.getHeaders(); String etag = responseHeaders.getETag(); long lastModifiedTimestamp = responseHeaders.getLastModified(); - if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.HEAD) { + if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.QUERY || request.getMethod() == HttpMethod.HEAD) { responseHeaders.remove(HttpHeaders.ETAG); responseHeaders.remove(HttpHeaders.LAST_MODIFIED); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index d64a3529ce3c..e2055784c44c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -152,7 +152,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator public ResourceHttpRequestHandler() { - super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); + super(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 201f7ffc9ce0..dcc63e8a0339 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -920,7 +920,7 @@ void testCorsMinimal() { CorsConfiguration config = configs.get("/**"); assertThat(config).isNotNull(); assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"*"}); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "HEAD", "POST"}); + assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "QUERY", "HEAD", "POST"}); assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[]{"*"}); assertThat(config.getExposedHeaders()).isNull(); assertThat(config.getAllowCredentials()).isNull(); @@ -953,7 +953,7 @@ void testCors() { assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123)); config = configs.get("/resources/**"); assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"https://domain1.com"}); - assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "HEAD", "POST"}); + assertThat(config.getAllowedMethods().toArray()).isEqualTo(new String[]{"GET", "QUERY", "HEAD", "POST"}); assertThat(config.getAllowedHeaders().toArray()).isEqualTo(new String[]{"*"}); assertThat(config.getExposedHeaders()).isNull(); assertThat(config.getAllowCredentials()).isNull(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java index 9c6d9320fda2..ae7eb4262fcf 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/ResourceHandlerFunctionTests.java @@ -174,7 +174,7 @@ void options() throws ServletException, IOException { ServerResponse response = this.handlerFunction.handle(request); assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS)); + assertThat(response.headers().getAllow()).isEqualTo(Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS)); MockHttpServletResponse servletResponse = new MockHttpServletResponse(); ModelAndView mav = response.writeTo(servletRequest, servletResponse, this.context); @@ -183,7 +183,7 @@ void options() throws ServletException, IOException { assertThat(servletResponse.getStatus()).isEqualTo(200); String allowHeader = servletResponse.getHeader("Allow"); String[] methods = StringUtils.tokenizeToStringArray(allowHeader, ","); - assertThat(methods).containsExactlyInAnyOrder("GET","HEAD","OPTIONS"); + assertThat(methods).containsExactlyInAnyOrder("GET","QUERY","HEAD","OPTIONS"); byte[] actualBytes = servletResponse.getContentAsByteArray(); assertThat(actualBytes).isEmpty(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java index 75d7eca575f9..8ca74beb2f02 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java @@ -183,7 +183,7 @@ void abortInterceptorInPreFlightRequestWithCorsConfig() throws Exception { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain.com"); - assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,HEAD"); + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,QUERY,HEAD"); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index a121a1a9486d..0a667b2e2e1a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -195,7 +195,8 @@ void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws testHttpOptions(mapping, "/person/1", "PUT,OPTIONS", null); testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS", null); testHttpOptions(mapping, "/something", "PUT,POST", null); - testHttpOptions(mapping, "/qux", "PATCH,GET,HEAD,OPTIONS", new MediaType("foo", "bar")); + testHttpOptions(mapping, "/qux", "PATCH,GET,QUERY,HEAD,OPTIONS", new MediaType("foo", "bar")); + testHttpOptions(mapping, "/quid", "QUERY,OPTIONS", null); } @PathPatternsParameterizedTest @@ -569,6 +570,11 @@ public String getBaz() { @RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar") public void patchBaz(String value) { } + + @RequestMapping(value = "/quid", method = RequestMethod.QUERY, consumes = "application/json", produces = "application/json") + public String query(@RequestBody String body) { + return "{}"; + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index 28db86a97983..ee23438fe8c4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -115,7 +115,7 @@ void supportsOptionsRequests() throws Exception { this.handler.handleRequest(this.request, this.response); assertThat(this.response.getStatus()).isEqualTo(200); - assertThat(this.response.getHeader("Allow")).isEqualTo("GET,HEAD,OPTIONS"); + assertThat(this.response.getHeader("Allow")).isEqualTo("GET,QUERY,HEAD,OPTIONS"); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java index 43efcda039d8..a28e1f69d19b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/support/WebContentGeneratorTests.java @@ -39,7 +39,7 @@ void getAllowHeaderWithConstructorTrue() { @Test void getAllowHeaderWithConstructorFalse() { WebContentGenerator generator = new TestWebContentGenerator(false); - assertThat(generator.getAllowHeader()).isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + assertThat(generator.getAllowHeader()).isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY"); } @Test @@ -59,7 +59,7 @@ void getAllowHeaderWithSupportedMethodsSetter() { void getAllowHeaderWithSupportedMethodsSetterEmpty() { WebContentGenerator generator = new TestWebContentGenerator(); generator.setSupportedMethods(); - assertThat(generator.getAllowHeader()).as("Effectively \"no restriction\" on supported methods").isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + assertThat(generator.getAllowHeader()).as("Effectively \"no restriction\" on supported methods").isEqualTo("GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY"); } @Test From c2348952cc0ee34c5d8e2ea2a7aff0010098fd32 Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Mon, 26 Aug 2024 21:11:18 -0500 Subject: [PATCH 2/5] Revert changes to RestTemplate --- .../web/client/RestOperations.java | 72 ---------------- .../web/client/RestTemplate.java | 62 -------------- .../web/client/RestOperationsExtensions.kt | 82 ------------------- .../web/client/RestTemplateTests.java | 41 +--------- 4 files changed, 3 insertions(+), 254 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index e4a649c4970d..440fad582a5e 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -485,78 +485,6 @@ T patchForObject(URI url, @Nullable Object request, Class responseType) */ Set optionsForAllow(URI url) throws RestClientException; - // QUERY - - /** - * Retrieve a representation by doing a QUERY on the specified URL. - * The response (if any) is converted and returned. - *

URI Template variables are expanded using the given URI variables, if any. - * @param url the URL - * @param responseType the type of the return value - * @param uriVariables the variables to expand the template - * @return the converted object - */ - @Nullable - T queryForObject(String url, @Nullable Object request, Class responseType, Object... uriVariables) throws RestClientException; - - /** - * Retrieve a representation by doing a QUERY on the URI template. - * The response (if any) is converted and returned. - *

URI Template variables are expanded using the given map. - * @param url the URL - * @param responseType the type of the return value - * @param uriVariables the map containing variables for the URI template - * @return the converted object - */ - @Nullable - T queryForObject(String url, @Nullable Object request, Class responseType, Map uriVariables) throws RestClientException; - - /** - * Retrieve a representation by doing a QUERY on the URL. - * The response (if any) is converted and returned. - * @param url the URL - * @param responseType the type of the return value - * @return the converted object - */ - @Nullable - T queryForObject(URI url, @Nullable Object request, Class responseType) throws RestClientException; - - /** - * Retrieve an entity by doing a QUERY on the specified URL. - * The response is converted and stored in a {@link ResponseEntity}. - *

URI Template variables are expanded using the given URI variables, if any. - * @param url the URL - * @param responseType the type of the return value - * @param uriVariables the variables to expand the template - * @return the entity - * @since 6.2 - */ - ResponseEntity queryForEntity(String url, @Nullable Object request, Class responseType, Object... uriVariables) - throws RestClientException; - - /** - * Retrieve a representation by doing a QUERY on the URI template. - * The response is converted and stored in a {@link ResponseEntity}. - *

URI Template variables are expanded using the given map. - * @param url the URL - * @param responseType the type of the return value - * @param uriVariables the map containing variables for the URI template - * @return the converted object - * @since 6.2 - */ - ResponseEntity queryForEntity(String url, @Nullable Object request, Class responseType, Map uriVariables) - throws RestClientException; - - /** - * Retrieve a representation by doing a GET on the URL. - * The response is converted and stored in a {@link ResponseEntity}. - * @param url the URL - * @param responseType the type of the return value - * @return the converted object - * @since 6.2 - */ - ResponseEntity queryForEntity(URI url, @Nullable Object request, Class responseType) throws RestClientException; - // exchange diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index a25169d92f26..e9784a5e4348 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -374,7 +374,6 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { /** * Return the configured {@link ObservationRegistry}. - * * @since 6.1 */ public ObservationRegistry getObservationRegistry() { @@ -671,67 +670,6 @@ public Set optionsForAllow(URI url) throws RestClientException { return (headers != null ? headers.getAllow() : Collections.emptySet()); } - // QUERY - - @Override - @Nullable - public T queryForObject(String url, @Nullable Object request, Class responseType, - Object... uriVariables) throws RestClientException { - - RequestCallback requestCallback = httpEntityCallback(request, responseType); - HttpMessageConverterExtractor responseExtractor = - new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); - return execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables); - } - - @Override - @Nullable - public T queryForObject(String url, @Nullable Object request, Class responseType, - Map uriVariables) throws RestClientException { - - RequestCallback requestCallback = httpEntityCallback(request, responseType); - HttpMessageConverterExtractor responseExtractor = - new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); - return execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables); - } - - @Override - @Nullable - public T queryForObject(URI url, @Nullable Object request, Class responseType) - throws RestClientException { - - RequestCallback requestCallback = httpEntityCallback(request, responseType); - HttpMessageConverterExtractor responseExtractor = - new HttpMessageConverterExtractor<>(responseType, getMessageConverters()); - return execute(url, HttpMethod.QUERY, requestCallback, responseExtractor); - } - - @Override - public ResponseEntity queryForEntity(String url, @Nullable Object request, - Class responseType, Object... uriVariables) throws RestClientException { - - RequestCallback requestCallback = httpEntityCallback(request, responseType); - ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); - return nonNull(execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables)); - } - - @Override - public ResponseEntity queryForEntity(String url, @Nullable Object request, - Class responseType, Map uriVariables) throws RestClientException { - - RequestCallback requestCallback = httpEntityCallback(request, responseType); - ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); - return nonNull(execute(url, HttpMethod.QUERY, requestCallback, responseExtractor, uriVariables)); - } - - @Override - public ResponseEntity queryForEntity(URI url, @Nullable Object request, Class responseType) - throws RestClientException { - - RequestCallback requestCallback = httpEntityCallback(request, responseType); - ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); - return nonNull(execute(url, HttpMethod.QUERY, requestCallback, responseExtractor)); - } // exchange diff --git a/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt b/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt index c9d5125d4c01..a6234cfb1402 100644 --- a/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt +++ b/spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt @@ -234,88 +234,6 @@ inline fun RestOperations.postForEntity(url: String, request: Any? = inline fun RestOperations.postForEntity(url: URI, request: Any? = null): ResponseEntity = postForEntity(url, request, T::class.java) -/** - * Extension for [RestOperations.queryForObject] providing a `queryForObject(...)` - * variant leveraging Kotlin reified type parameters. Like the original Java method, this - * extension is subject to type erasure. Use [exchange] if you need to retain actual - * generic type arguments. - * - * @author Mario Ruiz - * @since 6.2 - */ -@Throws(RestClientException::class) -inline fun RestOperations.queryForObject(url: String, request: Any? = null, - vararg uriVariables: Any): T = - queryForObject(url, request, T::class.java, *uriVariables) as T - -/** - * Extension for [RestOperations.queryForObject] providing a `queryForObject(...)` - * variant leveraging Kotlin reified type parameters. Like the original Java method, this - * extension is subject to type erasure. Use [exchange] if you need to retain actual - * generic type arguments. - * - * @author Mario Ruiz - * @since 6.2 - */ -@Throws(RestClientException::class) -inline fun RestOperations.queryForObject(url: String, request: Any? = null, - uriVariables: Map): T = - queryForObject(url, request, T::class.java, uriVariables) as T - -/** - * Extension for [RestOperations.queryForObject] providing a `queryForObject(...)` - * variant leveraging Kotlin reified type parameters. Like the original Java method, this - * extension is subject to type erasure. Use [exchange] if you need to retain actual - * generic type arguments. - * - * @author Mario Ruiz - * @since 6.2 - */ -@Throws(RestClientException::class) -inline fun RestOperations.queryForObject(url: URI, request: Any? = null): T = - queryForObject(url, request, T::class.java) as T - -/** - * Extension for [RestOperations.queryForEntity] providing a `queryForEntity(...)` - * variant leveraging Kotlin reified type parameters. Like the original Java method, this - * extension is subject to type erasure. Use [exchange] if you need to retain actual - * generic type arguments. - * - * @author Mario Ruiz - * @since 6.2 - */ -@Throws(RestClientException::class) -inline fun RestOperations.queryForEntity(url: String, request: Any? = null, - vararg uriVariables: Any): ResponseEntity = - queryForEntity(url, request, T::class.java, *uriVariables) - -/** - * Extension for [RestOperations.queryForEntity] providing a `queryForEntity(...)` - * variant leveraging Kotlin reified type parameters. Like the original Java method, this - * extension is subject to type erasure. Use [exchange] if you need to retain actual - * generic type arguments. - * - * @author Mario Ruiz - * @since 6.2 - */ -@Throws(RestClientException::class) -inline fun RestOperations.queryForEntity(url: String, request: Any? = null, - uriVariables: Map): ResponseEntity = - queryForEntity(url, request, T::class.java, uriVariables) - -/** - * Extension for [RestOperations.queryForEntity] providing a `queryForEntity(...)` - * variant leveraging Kotlin reified type parameters. Like the original Java method, this - * extension is subject to type erasure. Use [exchange] if you need to retain actual - * generic type arguments. - * - * @author Mario Ruiz - * @since 6.2 - */ -@Throws(RestClientException::class) -inline fun RestOperations.queryForEntity(url: URI, request: Any? = null): ResponseEntity = - queryForEntity(url, request, T::class.java) - /** * Extension for [RestOperations.exchange] providing an `exchange(...)` * variant leveraging Kotlin reified type parameters. This extension is not subject to diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 47a12a26041e..2970f8ce0823 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -43,6 +43,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.RequestEntity; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestInitializer; @@ -474,22 +475,6 @@ void postForEntityNull() throws Exception { verify(response).close(); } - @Test - void queryForObject() throws Exception { - mockTextPlainHttpMessageConverter(); - HttpHeaders requestHeaders = new HttpHeaders(); - mockSentRequest(QUERY, "https://example.com", requestHeaders); - mockResponseStatus(HttpStatus.OK); - String expected = "42"; - mockResponseBody(expected, MediaType.TEXT_PLAIN); - - String result = template.queryForObject("https://example.com", "Hello World", String.class); - assertThat(result).as("Invalid QUERY result").isEqualTo(expected); - assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); - - verify(response).close(); - } - @Test void queryForEntity() throws Exception { mockTextPlainHttpMessageConverter(); @@ -499,7 +484,7 @@ void queryForEntity() throws Exception { String expected = "42"; mockResponseBody(expected, MediaType.TEXT_PLAIN); - ResponseEntity result = template.queryForEntity("https://example.com", "Hello World", String.class); + ResponseEntity result = template.exchange(RequestEntity.query("https://example.com").body("Hello World"), String.class); assertThat(result.getBody()).as("Invalid QUERY result").isEqualTo(expected); assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN); assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); @@ -508,26 +493,6 @@ void queryForEntity() throws Exception { verify(response).close(); } - @Test - void queryForObjectNull() throws Exception { - mockTextPlainHttpMessageConverter(); - HttpHeaders requestHeaders = new HttpHeaders(); - mockSentRequest(QUERY, "https://example.com", requestHeaders); - mockResponseStatus(HttpStatus.OK); - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.setContentType(MediaType.TEXT_PLAIN); - responseHeaders.setContentLength(10); - given(response.getHeaders()).willReturn(responseHeaders); - given(response.getBody()).willReturn(InputStream.nullInputStream()); - given(converter.read(String.class, response)).willReturn(null); - - String result = template.queryForObject("https://example.com", null, String.class); - assertThat(result).as("Invalid QUERY result").isNull(); - assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0); - - verify(response).close(); - } - @Test void queryForEntityNull() throws Exception { mockTextPlainHttpMessageConverter(); @@ -541,7 +506,7 @@ void queryForEntityNull() throws Exception { given(response.getBody()).willReturn(InputStream.nullInputStream()); given(converter.read(String.class, response)).willReturn(null); - ResponseEntity result = template.queryForEntity("https://example.com", null, String.class); + ResponseEntity result = template.exchange("https://example.com",QUERY, null, String.class); assertThat(result.hasBody()).as("Invalid QUERY result").isFalse(); assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN); assertThat(requestHeaders.getContentLength()).as("Invalid content length").isEqualTo(0); From 5875661010a1f40108c190666b6d1eed5d4f223b Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Mon, 26 Aug 2024 21:13:19 -0500 Subject: [PATCH 3/5] Add more test scaffolding --- .../web/bind/annotation/RequestMapping.java | 2 +- .../client/AbstractMockWebServerTests.java | 27 ++++++++++++++++++- ...RequestMappingInfoHandlerMappingTests.java | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 1736b21ed172..b9f5aea94bf3 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -121,7 +121,7 @@ /** * The HTTP request methods to map to, narrowing the primary mapping: - * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. + * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE, QUERY. *

Supported at the type level as well as at the method level! * When used at the type level, all method-level mappings inherit this * HTTP method restriction. diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java index 18b8815e9595..05384e732423 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractMockWebServerTests.java @@ -74,7 +74,7 @@ void tearDown() throws Exception { private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) { if (request.getMethod().equals("OPTIONS")) { - return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE"); + return new MockResponse().setResponseCode(200).setHeader("Allow", "GET, QUERY, OPTIONS, HEAD, TRACE"); } Buffer buf = new Buffer(); buf.write(body); @@ -231,6 +231,28 @@ private MockResponse putRequest(RecordedRequest request, String expectedRequestC return new MockResponse().setResponseCode(202); } + private MockResponse queryRequest(RecordedRequest request, String expectedRequestContent, + String contentType, byte[] responseBody) { + + assertThat(request.getHeaders().values(CONTENT_LENGTH)).hasSize(1); + assertThat(Integer.parseInt(request.getHeader(CONTENT_LENGTH))).as("Invalid request content-length").isGreaterThan(0); + String requestContentType = request.getHeader(CONTENT_TYPE); + assertThat(requestContentType).as("No content-type").isNotNull(); + Charset charset = StandardCharsets.ISO_8859_1; + if (requestContentType.contains("charset=")) { + String charsetName = requestContentType.split("charset=")[1]; + charset = Charset.forName(charsetName); + } + assertThat(request.getBody().readString(charset)).as("Invalid request body").isEqualTo(expectedRequestContent); + Buffer buf = new Buffer(); + buf.write(responseBody); + return new MockResponse() + .setHeader(CONTENT_TYPE, contentType) + .setHeader(CONTENT_LENGTH, responseBody.length) + .setBody(buf) + .setResponseCode(200); + } + protected class TestDispatcher extends Dispatcher { @@ -293,6 +315,9 @@ else if (request.getPath().equals("/patch")) { else if (request.getPath().equals("/put")) { return putRequest(request, helloWorld); } + else if (request.getPath().equals("/query")) { + return queryRequest(request, helloWorld, textContentType.toString(), helloWorldBytes); + } return new MockResponse().setResponseCode(404); } catch (Throwable ex) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index 0a667b2e2e1a..c76f612189f7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -193,7 +193,7 @@ void getHandlerMediaTypeNotSupportedWithParseError(TestRequestMappingInfoHandler void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception { testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS", null); testHttpOptions(mapping, "/person/1", "PUT,OPTIONS", null); - testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS", null); + testHttpOptions(mapping, "/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY", null); testHttpOptions(mapping, "/something", "PUT,POST", null); testHttpOptions(mapping, "/qux", "PATCH,GET,QUERY,HEAD,OPTIONS", new MediaType("foo", "bar")); testHttpOptions(mapping, "/quid", "QUERY,OPTIONS", null); From ccf505a0ac9f8fddcc5adf0ed52f875d71a58dfd Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Tue, 1 Oct 2024 17:19:39 -0500 Subject: [PATCH 4/5] Add Accept-Query handling and remove versions from since --- .../org/springframework/http/HttpHeaders.java | 27 +++++++++++++++ .../org/springframework/http/HttpMethod.java | 2 +- .../springframework/http/RequestEntity.java | 2 +- .../HttpMediaTypeNotSupportedException.java | 3 ++ .../web/bind/annotation/QueryMapping.java | 2 +- .../UnsupportedMediaTypeStatusException.java | 3 ++ .../function/server/RouterFunctions.java | 4 +-- .../RequestMappingInfoHandlerMapping.java | 33 ++++++++++++------- ...RequestMappingInfoHandlerMappingTests.java | 12 +++++-- .../web/servlet/function/RouterFunctions.java | 4 +-- .../ResponseEntityExceptionHandlerTests.java | 17 ++++++++++ 11 files changed, 88 insertions(+), 21 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index cddc62ba5847..9a5f2e8f4ff7 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -108,6 +108,12 @@ public class HttpHeaders implements MultiValueMap, Serializable * @see Section 5.3.5 of RFC 7233 */ public static final String ACCEPT_RANGES = "Accept-Ranges"; + + /** + * The HTTP {@code Accept-Query} header field name. + * @see IETF Draft + */ + public static final String ACCEPT_QUERY = "Accept-Query"; /** * The CORS {@code Access-Control-Allow-Credentials} response header field name. * @see CORS W3C recommendation @@ -568,6 +574,27 @@ public List getAcceptPatch() { return MediaType.parseMediaTypes(get(ACCEPT_PATCH)); } + /** + * Set the list of acceptable {@linkplain MediaType media types} for + * {@code QUERY} methods, as specified by the {@code Accept-QUERY} header. + * @since x.x.x + */ + public void setAcceptQuery(List mediaTypes) { + set(ACCEPT_QUERY, MediaType.toString(mediaTypes)); + } + + /** + * Return the list of acceptable {@linkplain MediaType media types} for + * {@code QUERY} methods, as specified by the {@code Accept-Query} header. + *

Returns an empty list when the acceptable media types are unspecified. + * @since x.x.x + */ + public List getAcceptQuery() { + return MediaType.parseMediaTypes(get(ACCEPT_QUERY)); + } + + + /** * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header. */ diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index 3f7ec10a580e..24848e47b2d9 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -84,7 +84,7 @@ public final class HttpMethod implements Comparable, Serializable { /** * The HTTP method {@code QUERY}. - * @see IETF Draft + * @see IETF Draft */ public static final HttpMethod QUERY = new HttpMethod("QUERY"); diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index 6e2435f27f70..496d6480c565 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -349,7 +349,7 @@ public static BodyBuilder query(URI url) { * @param uriTemplate the uri template to use * @param uriVariables variables to expand the URI template with * @return the created builder - * @since 6.2 + * @since x.x.x */ public static BodyBuilder query(String uriTemplate, Object... uriVariables) { return method(HttpMethod.QUERY, uriTemplate, uriVariables); diff --git a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java index 4ce57138a5d3..3260978c341f 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java @@ -134,6 +134,9 @@ public HttpHeaders getHeaders() { if (HttpMethod.PATCH.equals(this.httpMethod)) { headers.setAcceptPatch(getSupportedMediaTypes()); } + if (HttpMethod.QUERY.equals(this.httpMethod)) { + headers.setAcceptQuery(getSupportedMediaTypes()); + } return headers; } diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java index 20ee9f9b3579..66eb8ce2ec2d 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/QueryMapping.java @@ -39,7 +39,7 @@ * such as {@code @GetMapping}, {@code @PutMapping}, etc. * * @author Mario Ruiz - * @since 6.2 + * @since x.x.x * @see GetMapping * @see PutMapping * @see PostMapping diff --git a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java index a9bed587b965..c81cee7df084 100644 --- a/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java +++ b/spring-web/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java @@ -165,6 +165,9 @@ public HttpHeaders getHeaders() { if (this.method == HttpMethod.PATCH) { headers.setAcceptPatch(this.supportedMediaTypes); } + if (this.method == HttpMethod.QUERY) { + headers.setAcceptQuery(this.supportedMediaTypes); + } return headers; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index ba0bc526b966..fa6b9249bab1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -700,7 +700,7 @@ public interface Builder { * Adds a route to the given handler function that handles HTTP {@code QUERY} requests. * @param handlerFunction the handler function to handle all {@code QUERY} requests * @return this builder - * @since 6.2 + * @since x.x.x */ Builder QUERY(HandlerFunction handlerFunction); @@ -722,7 +722,7 @@ public interface Builder { * @param handlerFunction the handler function to handle all {@code QUERY} requests that * match {@code predicate} * @return this builder - * @since 6.2 + * @since x.x.x * @see RequestPredicates */ Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 6ef4a21a4cff..8268c17ebe2e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -192,8 +192,9 @@ protected HandlerMethod handleNoMatch(Set infos, HttpMethod httpMethod = request.getMethod(); Set methods = helper.getAllowedMethods(); if (HttpMethod.OPTIONS.equals(httpMethod)) { - Set mediaTypes = helper.getConsumablePatchMediaTypes(); - HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes); + Set patchMediaTypes = helper.getConsumablePatchMediaTypes(); + Set queryMediaTypes = helper.getConsumableQueryMediaTypes(); + HttpOptionsHandler handler = new HttpOptionsHandler(methods, patchMediaTypes, queryMediaTypes); return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); } throw new MethodNotAllowedException(httpMethod, methods); @@ -326,14 +327,23 @@ public List>> getParamConditions() { * PATCH specified, or that have no methods at all. */ public Set getConsumablePatchMediaTypes() { - Set result = new LinkedHashSet<>(); - for (PartialMatch match : this.partialMatches) { - Set methods = match.getInfo().getMethodsCondition().getMethods(); - if (methods.isEmpty() || methods.contains(RequestMethod.PATCH)) { - result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes()); - } - } - return result; + return getConsumableMediaTypesForMethod(RequestMethod.PATCH); + } + + /** + * Return declared "consumable" types but only among those that have + * PATCH specified, or that have no methods at all. + */ + public Set getConsumableQueryMediaTypes() { + return getConsumableMediaTypesForMethod(RequestMethod.QUERY); + } + + private Set getConsumableMediaTypesForMethod(RequestMethod method) { + return this.partialMatches.stream() + .map(PartialMatch::getInfo) + .filter(info -> info.getMethodsCondition().getMethods().isEmpty() || info.getMethodsCondition().getMethods().contains(method)) + .flatMap(info -> info.getConsumesCondition().getConsumableMediaTypes().stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } @@ -403,9 +413,10 @@ private static class HttpOptionsHandler { private final HttpHeaders headers = new HttpHeaders(); - public HttpOptionsHandler(Set declaredMethods, Set acceptPatch) { + public HttpOptionsHandler(Set declaredMethods, Set acceptPatch, Set acceptQuery) { this.headers.setAllow(initAllowedHttpMethods(declaredMethods)); this.headers.setAcceptPatch(new ArrayList<>(acceptPatch)); + this.headers.setAcceptQuery(new ArrayList<>(acceptQuery)); } private static Set initAllowedHttpMethods(Set declaredMethods) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 0ba5b8ef598f..a76b6c29c55a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -377,7 +377,7 @@ private void testHttpMediaTypeNotSupportedException(String url) { .isEqualTo(Collections.singletonList(new MediaType("application", "xml")))); } - private void testHttpOptions(String requestURI, Set allowedMethods, @Nullable MediaType acceptPatch) { + private void testHttpOptions(String requestURI, Set allowedMethods, @Nullable MediaType acceptMediaType) { ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.options(requestURI)); HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); @@ -395,9 +395,15 @@ private void testHttpOptions(String requestURI, Set allowedMethods, HttpHeaders headers = (HttpHeaders) value; assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods); - if (acceptPatch != null && headers.getAllow().contains(HttpMethod.PATCH) ) { - assertThat(headers.getAcceptPatch()).containsExactly(acceptPatch); + if (acceptMediaType != null) { + if (headers.getAllow().contains(HttpMethod.PATCH)) { + assertThat(headers.getAcceptPatch()).containsExactly(acceptMediaType); + } + if (headers.getAllow().contains(HttpMethod.QUERY)) { + assertThat(headers.getAcceptQuery()).containsExactly(acceptMediaType); + } } + } private void testMediaTypeNotAcceptable(String url) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 0633a4d7ce39..d50396c2341b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java @@ -614,7 +614,7 @@ public interface Builder { * Adds a route to the given handler function that handles HTTP {@code QUERY} requests. * @param handlerFunction the handler function to handle all {@code QUERY} requests * @return this builder - * @since 5.3 + * @since x.x.x */ Builder QUERY(HandlerFunction handlerFunction); @@ -636,7 +636,7 @@ public interface Builder { * @param handlerFunction the handler function to handle all {@code QUERY} requests that * match {@code predicate} * @return this builder - * @since 5.3 + * @since x.x.x * @see RequestPredicates */ Builder QUERY(RequestPredicate predicate, HandlerFunction handlerFunction); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index 600b91f239ea..651ceaabc529 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -147,6 +147,23 @@ void patchHttpMediaTypeNotSupported() { assertThat(headers.getFirst(HttpHeaders.ACCEPT_PATCH)).isEqualTo("application/atom+xml, application/xml"); } + @Test + void queryHttpMediaTypeNotSupported() { + this.servletRequest = new MockHttpServletRequest("QUERY", "/"); + this.request = new ServletWebRequest(this.servletRequest, this.servletResponse); + + ResponseEntity entity = testException( + new HttpMediaTypeNotSupportedException( + MediaType.APPLICATION_JSON, + List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML), + HttpMethod.QUERY)); + + HttpHeaders headers = entity.getHeaders(); + assertThat(headers.getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml"); + assertThat(headers.getFirst(HttpHeaders.ACCEPT)).isEqualTo("application/atom+xml, application/xml"); + assertThat(headers.getFirst(HttpHeaders.ACCEPT_QUERY)).isEqualTo("application/atom+xml, application/xml"); + } + @Test void httpMediaTypeNotAcceptable() { testException(new HttpMediaTypeNotAcceptableException("")); From 75f16b431b3ea31e591c6dd3af955a511bba501b Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Tue, 1 Oct 2024 17:21:52 -0500 Subject: [PATCH 5/5] Small case fix --- .../src/main/java/org/springframework/http/HttpHeaders.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 9a5f2e8f4ff7..bc9cb28c13f9 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -576,7 +576,7 @@ public List getAcceptPatch() { /** * Set the list of acceptable {@linkplain MediaType media types} for - * {@code QUERY} methods, as specified by the {@code Accept-QUERY} header. + * {@code QUERY} methods, as specified by the {@code Accept-Query} header. * @since x.x.x */ public void setAcceptQuery(List mediaTypes) {