Compare commits

...

7 Commits

Author SHA1 Message Date
Mario Daniel Ruiz Saavedra 60c29d7d70
Merge 95c6c7a5bc into 7e6874ad80 2025-10-07 23:10:35 +03:00
Sam Brannen 7e6874ad80 Polish @⁠Autowired section of the reference manual
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run Details
2025-10-07 17:17:27 +02:00
Sam Brannen 097463e3b7 Remove outdated reference to JSR 305 in the reference documentation
Closes gh-35580
2025-10-07 17:10:40 +02:00
Mario Daniel Ruiz Saavedra 95c6c7a5bc Add query to RestTestClient
Signed-off-by: Mario Daniel Ruiz Saavedra <desiderantes93@gmail.com>
2025-09-23 21:38:15 -03:00
Mario Daniel Ruiz Saavedra 89a7ee5519 Fix tests
Signed-off-by: Mario Daniel Ruiz Saavedra <desiderantes93@gmail.com>
2025-09-23 21:38:13 -03:00
Mario Daniel Ruiz Saavedra 11f2634c02 Fix comment
Signed-off-by: Mario Daniel Ruiz Saavedra <desiderantes93@gmail.com>
2025-09-23 21:37:27 -03:00
Mario Daniel Ruiz Saavedra 49c2270c57 Add QUERY HTTP method
Signed-off-by: Mario Daniel Ruiz Saavedra <desiderantes93@gmail.com>
2025-09-23 21:37:24 -03:00
71 changed files with 792 additions and 96 deletions

View File

@ -37,18 +37,18 @@ Kotlin::
----
======
[NOTE]
[TIP]
====
As of Spring Framework 4.3, an `@Autowired` annotation on such a constructor is no longer
necessary if the target bean defines only one constructor to begin with. However, if
several constructors are available and there is no primary/default constructor, at least
one of the constructors must be annotated with `@Autowired` in order to instruct the
container which one to use. See the discussion on
xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] for details.
An `@Autowired` annotation on such a constructor is not necessary if the target bean
defines only one constructor. However, if several constructors are available and there is
no primary or default constructor, at least one of the constructors must be annotated
with `@Autowired` in order to instruct the container which one to use. See the discussion
on xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution]
for details.
====
You can also apply the `@Autowired` annotation to _traditional_ setter methods,
as the following example shows:
You can apply the `@Autowired` annotation to _traditional_ setter methods, as the
following example shows:
[tabs]
======
@ -84,8 +84,8 @@ Kotlin::
----
======
You can also apply the annotation to methods with arbitrary names and multiple
arguments, as the following example shows:
You can apply `@Autowired` to methods with arbitrary names and multiple arguments, as the
following example shows:
[tabs]
======
@ -176,14 +176,15 @@ Kotlin::
====
Make sure that your target components (for example, `MovieCatalog` or `CustomerPreferenceDao`)
are consistently declared by the type that you use for your `@Autowired`-annotated
injection points. Otherwise, injection may fail due to a "no type match found" error at runtime.
injection points. Otherwise, injection may fail due to a "no type match found" error at
runtime.
For XML-defined beans or component classes found via classpath scanning, the container
usually knows the concrete type up front. However, for `@Bean` factory methods, you need
to make sure that the declared return type is sufficiently expressive. For components
that implement several interfaces or for components potentially referred to by their
implementation type, consider declaring the most specific return type on your factory
method (at least as specific as required by the injection points referring to your bean).
implementation type, declare the most specific return type on your factory method (at
least as specific as required by the injection points referring to your bean).
====
.[[beans-autowired-annotation-self-injection]]Self Injection
@ -312,8 +313,8 @@ through `@Order` values in combination with `@Primary` on a single bean for each
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
The map values contain all beans of the expected type, and the keys contain the
corresponding bean names, as the following example shows:
The map values are all beans of the expected type, and the keys are the corresponding
bean names, as the following example shows:
[tabs]
======
@ -431,7 +432,7 @@ annotated constructor does not have to be public.
====
Alternatively, you can express the non-required nature of a particular dependency
through Java 8's `java.util.Optional`, as the following example shows:
through Java's `java.util.Optional`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@ -445,8 +446,8 @@ through Java 8's `java.util.Optional`, as the following example shows:
----
You can also use a parameter-level `@Nullable` annotation (of any kind in any package --
for example, `javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in
null-safety support:
for example, `org.jspecify.annotations.Nullable` from JSpecify) or just leverage Kotlin's
built-in null-safety support:
[tabs]
======
@ -477,13 +478,6 @@ Kotlin::
----
======
[NOTE]
====
A type-level `@Nullable` annotation such as from JSpecify is not supported in Spring
Framework 6.2 yet. You need to upgrade to Spring Framework 7.0 where the framework
detects type-level annotations and consistently declares JSpecify in its own codebase.
====
You can also use `@Autowired` for interfaces that are well-known resolvable
dependencies: `BeanFactory`, `ApplicationContext`, `Environment`, `ResourceLoader`,
`ApplicationEventPublisher`, and `MessageSource`. These interfaces and their extended
@ -528,5 +522,6 @@ class MovieRecommender {
The `@Autowired`, `@Inject`, `@Value`, and `@Resource` annotations are handled by Spring
`BeanPostProcessor` implementations. This means that you cannot apply these annotations
within your own `BeanPostProcessor` or `BeanFactoryPostProcessor` types (if any).
These types must be 'wired up' explicitly by using XML or a Spring `@Bean` method.
====

View File

@ -184,6 +184,16 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest {
return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
}
/**
* HTTP QUERY variant. See {@link #get(String, Object...)} for general info.
* @param urlTemplate a URL template; the resulting URL will be encoded
* @param uriVars zero or more URI variables
* @return the created builder
*/
public static BodyBuilder query(String urlTemplate, @Nullable Object... uriVars) {
return method(HttpMethod.QUERY, urlTemplate, uriVars);
}
/**
* Create a builder with the given HTTP method and a {@link URI}.
* @param method the HTTP method (GET, POST, etc)

View File

@ -165,6 +165,11 @@ class DefaultWebTestClient implements WebTestClient {
return methodInternal(HttpMethod.OPTIONS);
}
@Override
public RequestBodyUriSpec query() {
return methodInternal(HttpMethod.QUERY);
}
@Override
public RequestBodyUriSpec method(HttpMethod httpMethod) {
return methodInternal(httpMethod);

View File

@ -144,6 +144,12 @@ public interface WebTestClient {
*/
RequestHeadersUriSpec<?> options();
/**
* Prepare an HTTP QUERY request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec query();
/**
* Prepare a request for the specified {@code HttpMethod}.
* @return a spec for specifying the target URL

View File

@ -333,6 +333,20 @@ public final class MockMvcTester {
return method(HttpMethod.OPTIONS);
}
/**
* Prepare an HTTP QUERY request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@link MockMvcRequestBuilder#exchange() exchange()} to assign the
* result. To control the time to wait for asynchronous request to complete
* on a per-request basis, use
* {@link MockMvcRequestBuilder#exchange(Duration) exchange(Duration)}.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder query() {
return method(HttpMethod.QUERY);
}
/**
* Prepare a request for the specified {@code HttpMethod}.
* <p>The returned builder can be wrapped in {@code assertThat} to enable

View File

@ -112,6 +112,11 @@ class DefaultRestTestClient implements RestTestClient {
return methodInternal(HttpMethod.DELETE);
}
@Override
public RequestBodyUriSpec query() {
return methodInternal(HttpMethod.QUERY);
}
@Override
public RequestHeadersUriSpec<?> options() {
return methodInternal(HttpMethod.OPTIONS);

View File

@ -120,6 +120,12 @@ public interface RestTestClient {
*/
RequestHeadersUriSpec<?> delete();
/**
* Prepare an HTTP QUERY request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec query();
/**
* Prepare an HTTP OPTIONS request.
* @return a spec for specifying the target URL

View File

@ -175,6 +175,24 @@ public abstract class MockMvcRequestBuilders {
return new MockHttpServletRequestBuilder(HttpMethod.HEAD).uri(uri);
}
/**
* Create a {@link MockHttpServletRequestBuilder} for a QUERY request.
* @param uriTemplate a URI template; the resulting URI will be encoded
* @param uriVariables zero or more URI variables
*/
public static MockHttpServletRequestBuilder query(String uriTemplate, @Nullable Object... uriVariables) {
return new MockHttpServletRequestBuilder(HttpMethod.QUERY).uri(uriTemplate, uriVariables);
}
/**
* Create a {@link MockHttpServletRequestBuilder} for a QUERY request.
* @param uri the URI
* @since x.x.x
*/
public static MockHttpServletRequestBuilder query(URI uri) {
return new MockHttpServletRequestBuilder(HttpMethod.QUERY).uri(uri);
}
/**
* Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP method.
* @param method the HTTP method (GET, POST, etc.)

View File

@ -63,7 +63,7 @@ class RestTestClientTests {
class HttpMethods {
@ParameterizedTest
@ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"})
@ValueSource(strings = {"GET", "POST", "PUT", "DELETE", "PATCH", "QUERY", "HEAD"})
void testMethod(String method) {
RestTestClientTests.this.client.method(HttpMethod.valueOf(method)).uri("/test")
.exchange()
@ -124,10 +124,18 @@ class RestTestClientTests {
RestTestClientTests.this.client.options().uri("/test")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS")
.expectHeader().valueEquals("Allow", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,QUERY")
.expectBody().isEmpty();
}
@Test
void testQuery() {
RestTestClientTests.this.client.query().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.method").isEqualTo("QUERY");
}
}

View File

@ -32,6 +32,8 @@ import jakarta.servlet.http.Cookie;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
@ -418,18 +420,21 @@ class MockHttpServletRequestBuilderTests {
assertThat(request.getParameterMap().get("foo")).containsExactly("bar", "baz");
}
@Test
void requestParameterFromRequestBodyFormData() {
@ValueSource(strings = {"POST", "QUERY"})
@ParameterizedTest()
void requestParameterFromRequestBodyFormData(String methodName) {
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")
HttpMethod method = HttpMethod.valueOf(methodName);
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);
}
@Test

View File

@ -130,6 +130,12 @@ public class HttpHeaders implements Serializable {
* @see <a href="https://tools.ietf.org/html/rfc7233#section-2.3">Section 5.3.5 of RFC 7233</a>
*/
public static final String ACCEPT_RANGES = "Accept-Ranges";
/**
* The HTTP {@code Accept-Query} header field name.
* @see <a href="https://httpwg.org/http-extensions/draft-ietf-httpbis-safe-method-w-body.html">IETF Draft</a>
*/
public static final String ACCEPT_QUERY = "Accept-Query";
/**
* The CORS {@code Access-Control-Allow-Credentials} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
@ -635,6 +641,27 @@ public class HttpHeaders implements Serializable {
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<MediaType> 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.
* <p>Returns an empty list when the acceptable media types are unspecified.
* @since x.x.x
*/
public List<MediaType> getAcceptQuery() {
return MediaType.parseMediaTypes(get(ACCEPT_QUERY));
}
/**
* Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
*/

View File

@ -37,25 +37,25 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
/**
* The HTTP method {@code GET}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3">HTTP 1.1, section 9.3</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.1">HTTP Semantics, section 9.3.1</a>
*/
public static final HttpMethod GET = new HttpMethod("GET");
/**
* The HTTP method {@code HEAD}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4">HTTP 1.1, section 9.4</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.2">HTTP Semantics, section 9.3.2</a>
*/
public static final HttpMethod HEAD = new HttpMethod("HEAD");
/**
* The HTTP method {@code POST}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5">HTTP 1.1, section 9.5</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.3">HTTP Semantics, section 9.3.3</a>
*/
public static final HttpMethod POST = new HttpMethod("POST");
/**
* The HTTP method {@code PUT}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6">HTTP 1.1, section 9.6</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.4">HTTP Semantics, section 9.3.4</a>
*/
public static final HttpMethod PUT = new HttpMethod("PUT");
@ -67,23 +67,29 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
/**
* The HTTP method {@code DELETE}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7">HTTP 1.1, section 9.7</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5">HTTP Semantics, section 9.3.5</a>
*/
public static final HttpMethod DELETE = new HttpMethod("DELETE");
/**
* The HTTP method {@code OPTIONS}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2">HTTP 1.1, section 9.2</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.7">HTTP Semantics, section 9.3.7</a>
*/
public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");
/**
* The HTTP method {@code TRACE}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8">HTTP 1.1, section 9.8</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.8">HTTP Semantics, section 9.3.8</a>
*/
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 <a href="https://httpwg.org/http-extensions/draft-ietf-httpbis-safe-method-w-body.html">IETF Draft</a>
*/
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;
@ -97,7 +103,7 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
* 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}.
*
* <p>Note that the returned value does not include any HTTP methods defined
* in WebDav.
@ -124,6 +130,7 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
case "QUERY" -> QUERY;
default -> new HttpMethod(method);
};
}

View File

@ -382,6 +382,26 @@ public class RequestEntity<T> extends HttpEntity<T> {
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 x.x.x
*/
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

View File

@ -32,6 +32,7 @@ import org.apache.hc.client5.http.classic.methods.HttpPatch;
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;
@ -345,6 +346,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
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);
}

View File

@ -132,6 +132,9 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
if (HttpMethod.PATCH.equals(this.httpMethod)) {
headers.setAcceptPatch(getSupportedMediaTypes());
}
if (HttpMethod.QUERY.equals(this.httpMethod)) {
headers.setAcceptQuery(getSupportedMediaTypes());
}
return headers;
}

View File

@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
* @see PostMapping
* @see PutMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)

View File

@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)

View File

@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
* @see PostMapping
* @see PutMapping
* @see DeleteMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)

View File

@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)

View File

@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
* @see PostMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)

View File

@ -0,0 +1,104 @@
/*
* 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.
*
* <p>Specifically, {@code @QueryMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.QUERY)}.
*
* <p><strong>NOTE:</strong> 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 x.x.x
* @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 {};
/**
* Alias for {@link RequestMapping#version()}.
*/
@AliasFor(annotation = RequestMapping.class)
String version() default "";
}

View File

@ -51,8 +51,8 @@ import org.springframework.core.annotation.AliasFor;
* 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}.
*
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same element
@ -75,6 +75,7 @@ import org.springframework.core.annotation.AliasFor;
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ -125,7 +126,7 @@ public @interface RequestMapping {
/**
* 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.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit this
* HTTP method restriction.

View File

@ -26,7 +26,7 @@ import org.springframework.util.Assert;
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
*
* <p>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.
@ -39,7 +39,7 @@ import org.springframework.util.Assert;
*/
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 enum RequestMethod {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
case "QUERY" -> QUERY;
default -> null;
};
}
@ -92,6 +93,7 @@ public enum RequestMethod {
case DELETE -> HttpMethod.DELETE;
case OPTIONS -> HttpMethod.OPTIONS;
case TRACE -> HttpMethod.TRACE;
case QUERY -> HttpMethod.QUERY;
};
}

View File

@ -193,6 +193,11 @@ final class DefaultRestClient implements RestClient {
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");

View File

@ -125,6 +125,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

View File

@ -63,9 +63,9 @@ public class CorsConfiguration {
private static final List<String> DEFAULT_PERMIT_ALL = Collections.singletonList(ALL);
private static final List<HttpMethod> DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.HEAD);
private static final List<HttpMethod> DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private static final List<String> DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(),
private static final List<String> DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), HttpMethod.QUERY.name(),
HttpMethod.HEAD.name(), HttpMethod.POST.name());

View File

@ -151,7 +151,7 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
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));

View File

@ -161,6 +161,9 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
if (this.method == HttpMethod.PATCH) {
headers.setAcceptPatch(this.supportedMediaTypes);
}
if (this.method == HttpMethod.QUERY) {
headers.setAcceptQuery(this.supportedMediaTypes);
}
return headers;
}

View File

@ -66,7 +66,7 @@ import org.springframework.web.server.session.WebSessionManager;
*/
public class DefaultServerWebExchange implements ServerWebExchange {
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
private static final Set<HttpMethod> 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);

View File

@ -51,6 +51,7 @@ import org.springframework.web.util.UriBuilderFactory;
* <li>{@link PutExchange}
* <li>{@link PatchExchange}
* <li>{@link DeleteExchange}
* <li>{@link QueryExchange}
* </ul>
*
* <p>Supported method arguments:

View File

@ -0,0 +1,53 @@
package org.springframework.web.service.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* Shortcut for {@link HttpExchange @HttpExchange} for HTTP QUERY requests.
*
* @author Mario Ruiz
* @since x.x.x
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@HttpExchange(method = "QUERY")
public @interface QueryExchange {
/**
* Alias for {@link HttpExchange#value}.
*/
@AliasFor(annotation = HttpExchange.class)
String value() default "";
/**
* Alias for {@link HttpExchange#url()}.
*/
@AliasFor(annotation = HttpExchange.class)
String url() default "";
/**
* Alias for {@link HttpExchange#contentType()}.
*/
@AliasFor(annotation = HttpExchange.class)
String contentType() default "";
/**
* Alias for {@link HttpExchange#accept()}.
*/
@AliasFor(annotation = HttpExchange.class)
String[] accept() default {};
/**
* Alias for {@link HttpExchange#headers()}.
*/
@AliasFor(annotation = HttpExchange.class)
String[] headers() default {};
/**
* Alias for {@link HttpExchange#version()}.
*/
@AliasFor(annotation = HttpExchange.class)
String version() default "";
}

View File

@ -18,9 +18,11 @@ package org.springframework.web.client
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.web.client.queryForEntity
import java.lang.Class
import java.net.URI
import kotlin.reflect.KClass
@ -291,3 +293,45 @@ inline fun <reified T : Any> RestOperations.exchange(url: URI, method: HttpMetho
@Throws(RestClientException::class)
inline fun <reified T : Any> RestOperations.exchange(requestEntity: RequestEntity<*>): ResponseEntity<T> =
exchange(requestEntity, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RestOperations.postForEntity] providing a `postForEntity<Foo>(...)`
* 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 x.x.x
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> RestOperations.queryForEntity(url: String, request: Any? = null,
vararg uriVariables: Any?): ResponseEntity<T> =
exchange(url = url, method = HttpMethod.QUERY, requestEntity = HttpEntity(request, null as (HttpHeaders?) ), uriVariables= uriVariables)
/**
* Extension for [RestOperations.postForEntity] providing a `postForEntity<Foo>(...)`
* 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 x.x.x
*/
@Throws(RestClientException::class)
inline fun <reified T : Any> RestOperations.queryForEntity(url: String, request: Any? = null,
uriVariables: Map<String, *>): ResponseEntity<T> =
exchange(url = url, method = HttpMethod.QUERY, requestEntity = HttpEntity(request, null as (HttpHeaders?) ), uriVariables= uriVariables)
/**
* Extension for [RestOperations.postForEntity] providing a `postForEntity<Foo>(...)`
* 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 x.x.x
*/
@Throws(RestClientException::class)
inline fun <reified T: Any> RestOperations.queryForEntity(url: URI, request: Any? = null): ResponseEntity<T> =
exchange(url = url, method = HttpMethod.QUERY, requestEntity = HttpEntity(request, null as (HttpHeaders?) ))

View File

@ -44,12 +44,12 @@ class HttpMethodTests {
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

View File

@ -213,6 +213,11 @@ class ControllerMappingReflectiveProcessorTests {
void post(@RequestBody Request request) {
}
@QueryMapping
Response query(@RequestBody Request request) {
return new Response("response");
}
@PostMapping
void postForm(@ModelAttribute Request request) {
}
@ -247,6 +252,17 @@ class ControllerMappingReflectiveProcessorTests {
void postPartToConvert(@RequestPart Request request) {
}
@QueryMapping
HttpEntity<Response> querytHttpEntity(HttpEntity<Request> entity) {
return new HttpEntity<>(new Response("response"));
}
@QueryMapping
@SuppressWarnings("rawtypes")
HttpEntity queryRawHttpEntity(HttpEntity entity) {
return new HttpEntity(new Response("response"));
}
}
@RestController

View File

@ -75,7 +75,7 @@ abstract class AbstractMockWebServerTests {
private MockResponse getRequest(RecordedRequest request, byte[] body, @Nullable String contentType) {
if (request.getMethod().equals("OPTIONS")) {
return new MockResponse.Builder().code(200).setHeader("Allow", "GET, OPTIONS, HEAD, TRACE").build();
return new MockResponse.Builder().code(200).setHeader("Allow", "GET, QUERY, OPTIONS, HEAD, TRACE").build();
}
Buffer buf = new Buffer();
buf.write(body);
@ -240,6 +240,29 @@ abstract class AbstractMockWebServerTests {
return new MockResponse.Builder().code(202).build();
}
private MockResponse queryRequest(RecordedRequest request, String expectedRequestContent,
String contentType, byte[] responseBody) {
assertThat(request.getHeaders().values(CONTENT_LENGTH)).hasSize(1);
assertThat(Integer.parseInt(request.getHeaders().get(CONTENT_LENGTH))).as("Invalid request content-length").isGreaterThan(0);
String requestContentType = request.getHeaders().get(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().string(charset)).as("Invalid request body").isEqualTo(expectedRequestContent);
Buffer buf = new Buffer();
buf.write(responseBody);
return new MockResponse.Builder()
.code(200)
.setHeader(CONTENT_TYPE, contentType)
.setHeader(CONTENT_LENGTH, responseBody.length)
.body(buf)
.build();
}
protected class TestDispatcher extends Dispatcher {
@ -302,6 +325,9 @@ abstract class AbstractMockWebServerTests {
else if (request.getTarget().equals("/put")) {
return putRequest(request, helloWorld);
}
else if (request.getTarget().equals("/query")) {
return queryRequest(request, helloWorld, textContentType.toString(), helloWorldBytes);
}
return new MockResponse.Builder().code(404).build();
}
catch (Throwable ex) {

View File

@ -290,7 +290,7 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
setUpClient(clientHttpRequestFactory);
Set<HttpMethod> allowed = template.optionsForAllow(URI.create(baseUrl + "/get"));
assertThat(allowed).as("Invalid response").isEqualTo(Set.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE));
assertThat(allowed).as("Invalid response").isEqualTo(Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE));
}
@ParameterizedRestTemplateTest

View File

@ -43,6 +43,7 @@ import org.springframework.http.HttpMethod;
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;
@ -75,6 +76,7 @@ import static org.springframework.http.HttpMethod.OPTIONS;
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;
/**
@ -478,6 +480,46 @@ class RestTemplateTests {
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<String> 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);
assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
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<String> 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);
assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
verify(response).close();
}
@Test
void put() throws Exception {
mockTextPlainHttpMessageConverter();

View File

@ -140,7 +140,7 @@ class CorsConfigurationTests {
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 @@ class CorsConfigurationTests {
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 @@ class CorsConfigurationTests {
@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 @@ class CorsConfigurationTests {
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

View File

@ -252,7 +252,7 @@ class DefaultCorsProcessorTests {
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);
}

View File

@ -263,7 +263,7 @@ class DefaultCorsProcessorTests {
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

View File

@ -113,6 +113,10 @@ public class MvcAnnotationPredicates {
return new RequestMappingPredicate(path).method(RequestMethod.HEAD);
}
public static RequestMappingPredicate queryMapping(String... path) {
return new RequestMappingPredicate(path).method(RequestMethod.QUERY);
}
public static class ModelAttributePredicate implements Predicate<MethodParameter> {

View File

@ -177,6 +177,11 @@ final class DefaultWebClient implements WebClient {
return methodInternal(HttpMethod.OPTIONS);
}
@Override
public RequestHeadersUriSpec<?> query() {
return methodInternal(HttpMethod.QUERY);
}
@Override
public RequestBodyUriSpec method(HttpMethod httpMethod) {
return methodInternal(httpMethod);

View File

@ -123,6 +123,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

View File

@ -288,7 +288,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
*/
abstract static class AbstractServerResponse implements ServerResponse {
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private final HttpStatusCode statusCode;

View File

@ -287,6 +287,18 @@ public abstract class RequestPredicates {
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

View File

@ -43,7 +43,7 @@ import org.springframework.web.reactive.function.BodyInserters;
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
private static final Set<HttpMethod> 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 @@ class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
.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)

View File

@ -231,6 +231,30 @@ class RouterFunctionBuilder implements RouterFunctions.Builder {
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
}
// QUERY
@Override
public RouterFunctions.Builder QUERY(HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction);
}
@Override
public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder QUERY(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.QUERY(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction);
}
// other
@Override

View File

@ -692,6 +692,58 @@ public abstract class RouterFunctions {
*/
Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> 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 x.x.x
*/
Builder QUERY(HandlerFunction<ServerResponse> 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<ServerResponse> 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 x.x.x
* @see RequestPredicates
*/
Builder QUERY(RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
* that match the given pattern and predicate.
* <p>For instance, the following example routes QUERY requests for "/user" that contain JSON
* to the {@code addUser} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route()
* .QUERY("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
* .build();
* </pre>
* @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<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all requests that match the
* given predicate.

View File

@ -85,7 +85,7 @@ import org.springframework.web.util.pattern.PathPattern;
*/
public class ResourceWebHandler implements WebHandler, InitializingBean {
private static final Set<HttpMethod> SUPPORTED_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
private static final Set<HttpMethod> SUPPORTED_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);

View File

@ -157,6 +157,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
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;
}
@ -184,6 +187,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
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;
}

View File

@ -189,8 +189,9 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
HttpMethod httpMethod = request.getMethod();
Set<HttpMethod> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.equals(httpMethod)) {
Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
Set<MediaType> patchMediaTypes = helper.getConsumablePatchMediaTypes();
Set<MediaType> queryMediaTypes = helper.getConsumableQueryMediaTypes();
HttpOptionsHandler handler = new HttpOptionsHandler(methods, patchMediaTypes, queryMediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new MethodNotAllowedException(httpMethod, methods);
@ -323,14 +324,23 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
* PATCH specified, or that have no methods at all.
*/
public Set<MediaType> getConsumablePatchMediaTypes() {
Set<MediaType> result = new LinkedHashSet<>();
for (PartialMatch match : this.partialMatches) {
Set<RequestMethod> 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<MediaType> getConsumableQueryMediaTypes() {
return getConsumableMediaTypesForMethod(RequestMethod.QUERY);
}
private Set<MediaType> 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));
}
@ -400,9 +410,10 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
private final HttpHeaders headers = new HttpHeaders();
public HttpOptionsHandler(Set<HttpMethod> declaredMethods, Set<MediaType> acceptPatch) {
public HttpOptionsHandler(Set<HttpMethod> declaredMethods, Set<MediaType> acceptPatch, Set<MediaType> acceptQuery) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
this.headers.setAcceptQuery(new ArrayList<>(acceptQuery));
}
private static Set<HttpMethod> initAllowedHttpMethods(Set<HttpMethod> declaredMethods) {
@ -413,7 +424,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
}
else {
Set<HttpMethod> 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);

View File

@ -56,7 +56,7 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler implements HandlerResultHandler {
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
/**

View File

@ -141,7 +141,7 @@ class ResourceHandlerFunctionTests {
Mono<ServerResponse> responseMono = this.handlerFunction.handle(request);
Mono<Void> 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 @@ class ResourceHandlerFunctionTests {
.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();
}

View File

@ -377,7 +377,7 @@ class RequestMappingInfoHandlerMappingTests {
.isEqualTo(Collections.singletonList(new MediaType("application", "xml"))));
}
private void testHttpOptions(String requestURI, Set<HttpMethod> allowedMethods, @Nullable MediaType acceptPatch) {
private void testHttpOptions(String requestURI, Set<HttpMethod> allowedMethods, @Nullable MediaType acceptMediaType) {
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.options(requestURI));
HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
@ -395,9 +395,15 @@ class RequestMappingInfoHandlerMappingTests {
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) {

View File

@ -117,7 +117,7 @@ class GlobalCorsConfigIntegrationTests extends AbstractRequestMappingIntegration
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

View File

@ -43,7 +43,7 @@ import org.springframework.web.servlet.ModelAndView;
*/
abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
private static final Set<HttpMethod> SAFE_METHODS = Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);
private final HttpStatusCode statusCode;

View File

@ -286,6 +286,18 @@ public abstract class RequestPredicates {
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

View File

@ -41,7 +41,7 @@ import org.springframework.http.HttpStatus;
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
private static final Set<HttpMethod> SUPPORTED_METHODS =
Set.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
Set.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.OPTIONS);
private final Resource resource;
@ -63,6 +63,11 @@ class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
.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)

View File

@ -229,6 +229,30 @@ class RouterFunctionBuilder implements RouterFunctions.Builder {
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
}
// QUERY
@Override
public RouterFunctions.Builder QUERY(HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.method(HttpMethod.QUERY), handlerFunction);
}
@Override
public RouterFunctions.Builder QUERY(RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.method(HttpMethod.QUERY).and(predicate), handlerFunction);
}
@Override
public RouterFunctions.Builder QUERY(String pattern, HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.QUERY(pattern), handlerFunction);
}
@Override
public RouterFunctions.Builder QUERY(String pattern, RequestPredicate predicate,
HandlerFunction<ServerResponse> handlerFunction) {
return add(RequestPredicates.QUERY(pattern).and(predicate), handlerFunction);
}
// other
@Override

View File

@ -610,6 +610,57 @@ public abstract class RouterFunctions {
*/
Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> 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 x.x.x
*/
Builder QUERY(HandlerFunction<ServerResponse> 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<ServerResponse> 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 x.x.x
* @see RequestPredicates
*/
Builder QUERY(RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all HTTP {@code QUERY} requests
* that match the given pattern and predicate.
* <p>For instance, the following example routes QUERY requests for "/user" that contain JSON
* to the {@code addUser} method in {@code userController}:
* <pre class="code">
* RouterFunction&lt;ServerResponse&gt; route =
* RouterFunctions.route()
* .QUERY("/user", RequestPredicates.contentType(MediaType.APPLICATION_JSON), userController::addUser)
* .build();
* </pre>
* @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<ServerResponse> handlerFunction);
/**
* Adds a route to the given handler function that handles all requests that match the
* given predicate.

View File

@ -162,6 +162,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
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;
}
@ -189,6 +192,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
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;
}

View File

@ -519,7 +519,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
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);
}
}

View File

@ -243,7 +243,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
outputMessage.getServletResponse().setStatus(returnStatus.value());
if (returnStatus.value() == HttpStatus.OK.value()) {
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 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
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);
}

View File

@ -138,7 +138,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());
}

View File

@ -931,7 +931,7 @@ public class MvcNamespaceTests {
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();
@ -964,7 +964,7 @@ public class MvcNamespaceTests {
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();

View File

@ -175,7 +175,7 @@ class ResourceHandlerFunctionTests {
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);
@ -184,7 +184,7 @@ class ResourceHandlerFunctionTests {
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();
}

View File

@ -185,7 +185,7 @@ public class HandlerMethodMappingTests {
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

View File

@ -193,9 +193,10 @@ class RequestMappingInfoHandlerMappingTests {
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,HEAD,OPTIONS", new MediaType("foo", "bar"));
testHttpOptions(mapping, "/quid", "QUERY,HEAD,OPTIONS", null);
}
@PathPatternsParameterizedTest
@ -572,6 +573,11 @@ class RequestMappingInfoHandlerMappingTests {
@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 "{}";
}
}

View File

@ -149,6 +149,23 @@ class ResponseEntityExceptionHandlerTests {
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<Object> 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(""));

View File

@ -116,7 +116,7 @@ class ResourceHttpRequestHandlerTests {
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

View File

@ -39,7 +39,7 @@ class WebContentGeneratorTests {
@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 @@ class WebContentGeneratorTests {
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