Add QUERY HTTP method
Signed-off-by: Mario Daniel Ruiz Saavedra <desiderantes93@gmail.com>
This commit is contained in:
parent
b699b65b40
commit
6f9aa37b79
|
@ -184,6 +184,16 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest {
|
||||||
return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
|
return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST 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}.
|
* Create a builder with the given HTTP method and a {@link URI}.
|
||||||
* @param method the HTTP method (GET, POST, etc)
|
* @param method the HTTP method (GET, POST, etc)
|
||||||
|
|
|
@ -165,6 +165,11 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
return methodInternal(HttpMethod.OPTIONS);
|
return methodInternal(HttpMethod.OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestBodyUriSpec query() {
|
||||||
|
return methodInternal(HttpMethod.QUERY);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBodyUriSpec method(HttpMethod httpMethod) {
|
public RequestBodyUriSpec method(HttpMethod httpMethod) {
|
||||||
return methodInternal(httpMethod);
|
return methodInternal(httpMethod);
|
||||||
|
|
|
@ -146,6 +146,12 @@ public interface WebTestClient {
|
||||||
*/
|
*/
|
||||||
RequestHeadersUriSpec<?> options();
|
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}.
|
* Prepare a request for the specified {@code HttpMethod}.
|
||||||
* @return a spec for specifying the target URL
|
* @return a spec for specifying the target URL
|
||||||
|
|
|
@ -333,6 +333,20 @@ public final class MockMvcTester {
|
||||||
return method(HttpMethod.OPTIONS);
|
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}.
|
* Prepare a request for the specified {@code HttpMethod}.
|
||||||
* <p>The returned builder can be wrapped in {@code assertThat} to enable
|
* <p>The returned builder can be wrapped in {@code assertThat} to enable
|
||||||
|
|
|
@ -175,6 +175,24 @@ public abstract class MockMvcRequestBuilders {
|
||||||
return new MockHttpServletRequestBuilder(HttpMethod.HEAD).uri(uri);
|
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.
|
* Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP method.
|
||||||
* @param method the HTTP method (GET, POST, etc.)
|
* @param method the HTTP method (GET, POST, etc.)
|
||||||
|
|
|
@ -32,6 +32,7 @@ import jakarta.servlet.http.Cookie;
|
||||||
import org.assertj.core.api.ThrowingConsumer;
|
import org.assertj.core.api.ThrowingConsumer;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
@ -53,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
import static org.assertj.core.api.Assertions.entry;
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
import static org.springframework.http.HttpMethod.GET;
|
import static org.springframework.http.HttpMethod.GET;
|
||||||
import static org.springframework.http.HttpMethod.POST;
|
import static org.springframework.http.HttpMethod.POST;
|
||||||
|
import static org.springframework.http.HttpMethod.QUERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for building a {@link MockHttpServletRequest} with
|
* Tests for building a {@link MockHttpServletRequest} with
|
||||||
|
@ -391,17 +393,20 @@ class MockHttpServletRequestBuilderTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ParameterizedTest()
|
||||||
void requestParameterFromRequestBodyFormData() {
|
void requestParameterFromRequestBodyFormData() {
|
||||||
String contentType = "application/x-www-form-urlencoded;charset=UTF-8";
|
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";
|
String body = "name+1=value+1&name+2=value+A&name+2=value+B&name+3";
|
||||||
|
|
||||||
MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST).uri("/foo")
|
for (HttpMethod method : List.of(POST, QUERY)) {
|
||||||
.contentType(contentType).content(body.getBytes(UTF_8))
|
MockHttpServletRequest request = new MockHttpServletRequestBuilder(method).uri("/foo")
|
||||||
.buildRequest(this.servletContext);
|
.contentType(contentType).content(body.getBytes(UTF_8))
|
||||||
|
.buildRequest(this.servletContext);
|
||||||
|
|
||||||
assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1");
|
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 2")).containsExactly("value A", "value B");
|
||||||
assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null);
|
assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -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>
|
* @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";
|
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.
|
* The CORS {@code Access-Control-Allow-Credentials} response header field name.
|
||||||
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
|
* @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));
|
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.
|
* Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -37,25 +37,25 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP method {@code GET}.
|
* 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");
|
public static final HttpMethod GET = new HttpMethod("GET");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP method {@code HEAD}.
|
* 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");
|
public static final HttpMethod HEAD = new HttpMethod("HEAD");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP method {@code POST}.
|
* 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");
|
public static final HttpMethod POST = new HttpMethod("POST");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP method {@code PUT}.
|
* 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");
|
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}.
|
* 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");
|
public static final HttpMethod DELETE = new HttpMethod("DELETE");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP method {@code OPTIONS}.
|
* 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");
|
public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP method {@code TRACE}.
|
* 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");
|
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;
|
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,
|
* Returns an array containing the standard HTTP methods. Specifically,
|
||||||
* this method returns an array containing {@link #GET}, {@link #HEAD},
|
* this method returns an array containing {@link #GET}, {@link #HEAD},
|
||||||
* {@link #POST}, {@link #PUT}, {@link #PATCH}, {@link #DELETE},
|
* {@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
|
* <p>Note that the returned value does not include any HTTP methods defined
|
||||||
* in WebDav.
|
* in WebDav.
|
||||||
|
@ -124,6 +130,7 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {
|
||||||
case "DELETE" -> DELETE;
|
case "DELETE" -> DELETE;
|
||||||
case "OPTIONS" -> OPTIONS;
|
case "OPTIONS" -> OPTIONS;
|
||||||
case "TRACE" -> TRACE;
|
case "TRACE" -> TRACE;
|
||||||
|
case "QUERY" -> QUERY;
|
||||||
default -> new HttpMethod(method);
|
default -> new HttpMethod(method);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -382,6 +382,26 @@ public class RequestEntity<T> extends HttpEntity<T> {
|
||||||
return method(HttpMethod.POST, uriTemplate, 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 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.
|
* Create an HTTP PUT builder with the given url.
|
||||||
* @param url the URL
|
* @param url the URL
|
||||||
|
|
|
@ -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.HttpPost;
|
||||||
import org.apache.hc.client5.http.classic.methods.HttpPut;
|
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.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.Configurable;
|
||||||
import org.apache.hc.client5.http.config.RequestConfig;
|
import org.apache.hc.client5.http.config.RequestConfig;
|
||||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||||
|
@ -345,6 +346,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
|
||||||
else if (HttpMethod.TRACE.equals(httpMethod)) {
|
else if (HttpMethod.TRACE.equals(httpMethod)) {
|
||||||
return new HttpTrace(uri);
|
return new HttpTrace(uri);
|
||||||
}
|
}
|
||||||
|
else if (HttpMethod.QUERY.equals(httpMethod)) {
|
||||||
|
return new HttpUriRequestBase(HttpMethod.QUERY.name(), uri);
|
||||||
|
}
|
||||||
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
|
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,9 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
|
||||||
if (HttpMethod.PATCH.equals(this.httpMethod)) {
|
if (HttpMethod.PATCH.equals(this.httpMethod)) {
|
||||||
headers.setAcceptPatch(getSupportedMediaTypes());
|
headers.setAcceptPatch(getSupportedMediaTypes());
|
||||||
}
|
}
|
||||||
|
if (HttpMethod.QUERY.equals(this.httpMethod)) {
|
||||||
|
headers.setAcceptQuery(getSupportedMediaTypes());
|
||||||
|
}
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* @see PostMapping
|
* @see PostMapping
|
||||||
* @see PutMapping
|
* @see PutMapping
|
||||||
* @see PatchMapping
|
* @see PatchMapping
|
||||||
|
* @see QueryMapping
|
||||||
* @see RequestMapping
|
* @see RequestMapping
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* @see PutMapping
|
* @see PutMapping
|
||||||
* @see DeleteMapping
|
* @see DeleteMapping
|
||||||
* @see PatchMapping
|
* @see PatchMapping
|
||||||
|
* @see QueryMapping
|
||||||
* @see RequestMapping
|
* @see RequestMapping
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* @see PostMapping
|
* @see PostMapping
|
||||||
* @see PutMapping
|
* @see PutMapping
|
||||||
* @see DeleteMapping
|
* @see DeleteMapping
|
||||||
|
* @see QueryMapping
|
||||||
* @see RequestMapping
|
* @see RequestMapping
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* @see PutMapping
|
* @see PutMapping
|
||||||
* @see DeleteMapping
|
* @see DeleteMapping
|
||||||
* @see PatchMapping
|
* @see PatchMapping
|
||||||
|
* @see QueryMapping
|
||||||
* @see RequestMapping
|
* @see RequestMapping
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* @see PostMapping
|
* @see PostMapping
|
||||||
* @see DeleteMapping
|
* @see DeleteMapping
|
||||||
* @see PatchMapping
|
* @see PatchMapping
|
||||||
|
* @see QueryMapping
|
||||||
* @see RequestMapping
|
* @see RequestMapping
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
|
|
|
@ -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 "";
|
||||||
|
|
||||||
|
}
|
|
@ -51,8 +51,8 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* at the method level. In most cases, at the method level applications will
|
* at the method level. In most cases, at the method level applications will
|
||||||
* prefer to use one of the HTTP method specific variants
|
* prefer to use one of the HTTP method specific variants
|
||||||
* {@link GetMapping @GetMapping}, {@link PostMapping @PostMapping},
|
* {@link GetMapping @GetMapping}, {@link PostMapping @PostMapping},
|
||||||
* {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or
|
* {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping},
|
||||||
* {@link PatchMapping @PatchMapping}.
|
* {@link PatchMapping @PatchMapping}, or {@link QueryMapping}.
|
||||||
*
|
*
|
||||||
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
|
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
|
||||||
* other {@code @RequestMapping} annotations that are declared on the same element
|
* other {@code @RequestMapping} annotations that are declared on the same element
|
||||||
|
@ -75,6 +75,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||||
* @see PutMapping
|
* @see PutMapping
|
||||||
* @see DeleteMapping
|
* @see DeleteMapping
|
||||||
* @see PatchMapping
|
* @see PatchMapping
|
||||||
|
* @see QueryMapping
|
||||||
*/
|
*/
|
||||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@ -121,7 +122,7 @@ public @interface RequestMapping {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP request methods to map to, narrowing the primary mapping:
|
* 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>
|
* <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
|
* When used at the type level, all method-level mappings inherit this
|
||||||
* HTTP method restriction.
|
* HTTP method restriction.
|
||||||
|
|
|
@ -26,7 +26,7 @@ import org.springframework.util.Assert;
|
||||||
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
|
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
|
||||||
*
|
*
|
||||||
* <p>Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
|
* <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
|
* process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
|
||||||
* told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
|
* told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
|
||||||
* and "dispatchTraceRequest" properties, switching them to "true" if necessary.
|
* and "dispatchTraceRequest" properties, switching them to "true" if necessary.
|
||||||
|
@ -39,7 +39,7 @@ import org.springframework.util.Assert;
|
||||||
*/
|
*/
|
||||||
public enum RequestMethod {
|
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 "DELETE" -> DELETE;
|
||||||
case "OPTIONS" -> OPTIONS;
|
case "OPTIONS" -> OPTIONS;
|
||||||
case "TRACE" -> TRACE;
|
case "TRACE" -> TRACE;
|
||||||
|
case "QUERY" -> QUERY;
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -92,6 +93,7 @@ public enum RequestMethod {
|
||||||
case DELETE -> HttpMethod.DELETE;
|
case DELETE -> HttpMethod.DELETE;
|
||||||
case OPTIONS -> HttpMethod.OPTIONS;
|
case OPTIONS -> HttpMethod.OPTIONS;
|
||||||
case TRACE -> HttpMethod.TRACE;
|
case TRACE -> HttpMethod.TRACE;
|
||||||
|
case QUERY -> HttpMethod.QUERY;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -193,6 +193,11 @@ final class DefaultRestClient implements RestClient {
|
||||||
return methodInternal(HttpMethod.OPTIONS);
|
return methodInternal(HttpMethod.OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestHeadersUriSpec<?> query() {
|
||||||
|
return methodInternal(HttpMethod.QUERY);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBodyUriSpec method(HttpMethod method) {
|
public RequestBodyUriSpec method(HttpMethod method) {
|
||||||
Assert.notNull(method, "HttpMethod must not be null");
|
Assert.notNull(method, "HttpMethod must not be null");
|
||||||
|
|
|
@ -123,6 +123,12 @@ public interface RestClient {
|
||||||
*/
|
*/
|
||||||
RequestHeadersUriSpec<?> options();
|
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}.
|
* Start building a request for the given {@code HttpMethod}.
|
||||||
* @return a spec for specifying the target URL
|
* @return a spec for specifying the target URL
|
||||||
|
|
|
@ -63,9 +63,9 @@ public class CorsConfiguration {
|
||||||
|
|
||||||
private static final List<String> DEFAULT_PERMIT_ALL = Collections.singletonList(ALL);
|
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());
|
HttpMethod.HEAD.name(), HttpMethod.POST.name());
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
if (!response.isCommitted() &&
|
if (!response.isCommitted() &&
|
||||||
responseStatusCode >= 200 && responseStatusCode < 300 &&
|
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);
|
String cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL);
|
||||||
return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE));
|
return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE));
|
||||||
|
|
|
@ -161,6 +161,9 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException
|
||||||
if (this.method == HttpMethod.PATCH) {
|
if (this.method == HttpMethod.PATCH) {
|
||||||
headers.setAcceptPatch(this.supportedMediaTypes);
|
headers.setAcceptPatch(this.supportedMediaTypes);
|
||||||
}
|
}
|
||||||
|
if (this.method == HttpMethod.QUERY) {
|
||||||
|
headers.setAcceptQuery(this.supportedMediaTypes);
|
||||||
|
}
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ import org.springframework.web.server.session.WebSessionManager;
|
||||||
*/
|
*/
|
||||||
public class DefaultServerWebExchange implements ServerWebExchange {
|
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 =
|
private static final ResolvableType FORM_DATA_TYPE =
|
||||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
|
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
|
||||||
|
|
|
@ -51,6 +51,7 @@ import org.springframework.web.util.UriBuilderFactory;
|
||||||
* <li>{@link PutExchange}
|
* <li>{@link PutExchange}
|
||||||
* <li>{@link PatchExchange}
|
* <li>{@link PatchExchange}
|
||||||
* <li>{@link DeleteExchange}
|
* <li>{@link DeleteExchange}
|
||||||
|
* <li>{@link QueryExchange}
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>Supported method arguments:
|
* <p>Supported method arguments:
|
||||||
|
|
|
@ -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 "";
|
||||||
|
}
|
|
@ -18,9 +18,11 @@ package org.springframework.web.client
|
||||||
|
|
||||||
import org.springframework.core.ParameterizedTypeReference
|
import org.springframework.core.ParameterizedTypeReference
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.RequestEntity
|
import org.springframework.http.RequestEntity
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.client.queryForEntity
|
||||||
import java.lang.Class
|
import java.lang.Class
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
@ -291,3 +293,45 @@ inline fun <reified T : Any> RestOperations.exchange(url: URI, method: HttpMetho
|
||||||
@Throws(RestClientException::class)
|
@Throws(RestClientException::class)
|
||||||
inline fun <reified T : Any> RestOperations.exchange(requestEntity: RequestEntity<*>): ResponseEntity<T> =
|
inline fun <reified T : Any> RestOperations.exchange(requestEntity: RequestEntity<*>): ResponseEntity<T> =
|
||||||
exchange(requestEntity, object : ParameterizedTypeReference<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?) ))
|
||||||
|
|
||||||
|
|
|
@ -44,12 +44,12 @@ class HttpMethodTests {
|
||||||
void values() {
|
void values() {
|
||||||
HttpMethod[] values = HttpMethod.values();
|
HttpMethod[] values = HttpMethod.values();
|
||||||
assertThat(values).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT,
|
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
|
// check defensive copy
|
||||||
values[0] = HttpMethod.POST;
|
values[0] = HttpMethod.POST;
|
||||||
assertThat(HttpMethod.values()).containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT,
|
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
|
@Test
|
||||||
|
|
|
@ -213,6 +213,11 @@ class ControllerMappingReflectiveProcessorTests {
|
||||||
void post(@RequestBody Request request) {
|
void post(@RequestBody Request request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@QueryMapping
|
||||||
|
Response query(@RequestBody Request request) {
|
||||||
|
return new Response("response");
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
void postForm(@ModelAttribute Request request) {
|
void postForm(@ModelAttribute Request request) {
|
||||||
}
|
}
|
||||||
|
@ -247,6 +252,17 @@ class ControllerMappingReflectiveProcessorTests {
|
||||||
void postPartToConvert(@RequestPart Request request) {
|
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
|
@RestController
|
||||||
|
|
|
@ -74,7 +74,7 @@ abstract class AbstractMockWebServerTests {
|
||||||
|
|
||||||
private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) {
|
private MockResponse getRequest(RecordedRequest request, byte[] body, String contentType) {
|
||||||
if (request.getMethod().equals("OPTIONS")) {
|
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();
|
Buffer buf = new Buffer();
|
||||||
buf.write(body);
|
buf.write(body);
|
||||||
|
@ -231,6 +231,28 @@ abstract class AbstractMockWebServerTests {
|
||||||
return new MockResponse().setResponseCode(202);
|
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 {
|
protected class TestDispatcher extends Dispatcher {
|
||||||
|
|
||||||
|
@ -293,6 +315,9 @@ abstract class AbstractMockWebServerTests {
|
||||||
else if (request.getPath().equals("/put")) {
|
else if (request.getPath().equals("/put")) {
|
||||||
return putRequest(request, helloWorld);
|
return putRequest(request, helloWorld);
|
||||||
}
|
}
|
||||||
|
else if (request.getPath().equals("/query")) {
|
||||||
|
return queryRequest(request, helloWorld, textContentType.toString(), helloWorldBytes);
|
||||||
|
}
|
||||||
return new MockResponse().setResponseCode(404);
|
return new MockResponse().setResponseCode(404);
|
||||||
}
|
}
|
||||||
catch (Throwable ex) {
|
catch (Throwable ex) {
|
||||||
|
|
|
@ -290,7 +290,7 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
|
||||||
setUpClient(clientHttpRequestFactory);
|
setUpClient(clientHttpRequestFactory);
|
||||||
|
|
||||||
Set<HttpMethod> allowed = template.optionsForAllow(URI.create(baseUrl + "/get"));
|
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
|
@ParameterizedRestTemplateTest
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
import org.springframework.http.client.ClientHttpRequest;
|
import org.springframework.http.client.ClientHttpRequest;
|
||||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
import org.springframework.http.client.ClientHttpRequestInitializer;
|
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.PATCH;
|
||||||
import static org.springframework.http.HttpMethod.POST;
|
import static org.springframework.http.HttpMethod.POST;
|
||||||
import static org.springframework.http.HttpMethod.PUT;
|
import static org.springframework.http.HttpMethod.PUT;
|
||||||
|
import static org.springframework.http.HttpMethod.QUERY;
|
||||||
import static org.springframework.http.MediaType.parseMediaType;
|
import static org.springframework.http.MediaType.parseMediaType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -478,6 +480,46 @@ class RestTemplateTests {
|
||||||
verify(response).close();
|
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
|
@Test
|
||||||
void put() throws Exception {
|
void put() throws Exception {
|
||||||
mockTextPlainHttpMessageConverter();
|
mockTextPlainHttpMessageConverter();
|
||||||
|
|
|
@ -140,7 +140,7 @@ class CorsConfigurationTests {
|
||||||
assertThat(config.getAllowedHeaders()).containsExactly("*");
|
assertThat(config.getAllowedHeaders()).containsExactly("*");
|
||||||
assertThat(combinedConfig).isNotNull();
|
assertThat(combinedConfig).isNotNull();
|
||||||
assertThat(combinedConfig.getAllowedMethods())
|
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();
|
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
|
||||||
|
|
||||||
combinedConfig = new CorsConfiguration().combine(config);
|
combinedConfig = new CorsConfiguration().combine(config);
|
||||||
|
@ -148,7 +148,7 @@ class CorsConfigurationTests {
|
||||||
assertThat(config.getAllowedHeaders()).containsExactly("*");
|
assertThat(config.getAllowedHeaders()).containsExactly("*");
|
||||||
assertThat(combinedConfig).isNotNull();
|
assertThat(combinedConfig).isNotNull();
|
||||||
assertThat(combinedConfig.getAllowedMethods())
|
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();
|
assertThat(combinedConfig.getExposedHeaders()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,7 +394,9 @@ class CorsConfigurationTests {
|
||||||
@Test
|
@Test
|
||||||
void checkMethodAllowed() {
|
void checkMethodAllowed() {
|
||||||
CorsConfiguration config = new CorsConfiguration();
|
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");
|
config.addAllowedMethod("GET");
|
||||||
assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET);
|
assertThat(config.checkHttpMethod(HttpMethod.GET)).containsExactly(HttpMethod.GET);
|
||||||
|
@ -450,7 +452,7 @@ class CorsConfigurationTests {
|
||||||
|
|
||||||
assertThat(config.getAllowedOrigins()).containsExactly("*", "https://domain.com");
|
assertThat(config.getAllowedOrigins()).containsExactly("*", "https://domain.com");
|
||||||
assertThat(config.getAllowedHeaders()).containsExactly("*", "header1");
|
assertThat(config.getAllowedHeaders()).containsExactly("*", "header1");
|
||||||
assertThat(config.getAllowedMethods()).containsExactly("GET", "HEAD", "POST", "PATCH");
|
assertThat(config.getAllowedMethods()).containsExactly("GET", "QUERY", "HEAD", "POST", "PATCH");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -252,7 +252,7 @@ class DefaultCorsProcessorTests {
|
||||||
|
|
||||||
this.processor.processRequest(this.conf, this.request, this.response);
|
this.processor.processRequest(this.conf, this.request, this.response);
|
||||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
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,
|
assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN,
|
||||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,7 +263,7 @@ class DefaultCorsProcessorTests {
|
||||||
assertThat(response.getStatusCode()).isNull();
|
assertThat(response.getStatusCode()).isNull();
|
||||||
assertThat(response.getHeaders().get(VARY)).contains(ORIGIN,
|
assertThat(response.getHeaders().get(VARY)).contains(ORIGIN,
|
||||||
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
|
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
|
@Test
|
||||||
|
|
|
@ -113,6 +113,10 @@ public class MvcAnnotationPredicates {
|
||||||
return new RequestMappingPredicate(path).method(RequestMethod.HEAD);
|
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> {
|
public static class ModelAttributePredicate implements Predicate<MethodParameter> {
|
||||||
|
|
|
@ -177,6 +177,11 @@ final class DefaultWebClient implements WebClient {
|
||||||
return methodInternal(HttpMethod.OPTIONS);
|
return methodInternal(HttpMethod.OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestHeadersUriSpec<?> query() {
|
||||||
|
return methodInternal(HttpMethod.QUERY);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBodyUriSpec method(HttpMethod httpMethod) {
|
public RequestBodyUriSpec method(HttpMethod httpMethod) {
|
||||||
return methodInternal(httpMethod);
|
return methodInternal(httpMethod);
|
||||||
|
|
|
@ -123,6 +123,12 @@ public interface WebClient {
|
||||||
*/
|
*/
|
||||||
RequestHeadersUriSpec<?> options();
|
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}.
|
* Start building a request for the given {@code HttpMethod}.
|
||||||
* @return a spec for specifying the target URL
|
* @return a spec for specifying the target URL
|
||||||
|
|
|
@ -288,7 +288,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
|
||||||
*/
|
*/
|
||||||
abstract static class AbstractServerResponse implements ServerResponse {
|
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;
|
private final HttpStatusCode statusCode;
|
||||||
|
|
||||||
|
|
|
@ -266,6 +266,18 @@ public abstract class RequestPredicates {
|
||||||
return method(HttpMethod.OPTIONS).and(path(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.
|
* 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
|
* @param extension the path extension to match against, ignoring case
|
||||||
|
|
|
@ -43,7 +43,7 @@ import org.springframework.web.reactive.function.BodyInserters;
|
||||||
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
||||||
|
|
||||||
private static final Set<HttpMethod> SUPPORTED_METHODS =
|
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;
|
private final Resource resource;
|
||||||
|
@ -66,6 +66,12 @@ class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
||||||
.build()
|
.build()
|
||||||
.map(response -> response);
|
.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)) {
|
else if (HttpMethod.HEAD.equals(method)) {
|
||||||
Resource headResource = new HeadMethodResource(this.resource);
|
Resource headResource = new HeadMethodResource(this.resource);
|
||||||
return EntityResponse.fromObject(headResource)
|
return EntityResponse.fromObject(headResource)
|
||||||
|
|
|
@ -231,6 +231,30 @@ class RouterFunctionBuilder implements RouterFunctions.Builder {
|
||||||
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
|
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
|
// other
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -696,6 +696,58 @@ public abstract class RouterFunctions {
|
||||||
*/
|
*/
|
||||||
Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
|
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<ServerResponse> 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
|
* Adds a route to the given handler function that handles all requests that match the
|
||||||
* given predicate.
|
* given predicate.
|
||||||
|
|
|
@ -85,7 +85,7 @@ import org.springframework.web.util.pattern.PathPattern;
|
||||||
*/
|
*/
|
||||||
public class ResourceWebHandler implements WebHandler, InitializingBean {
|
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);
|
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
|
||||||
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
|
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
|
||||||
return requestMethodConditionCache.get(HttpMethod.GET);
|
return requestMethodConditionCache.get(HttpMethod.GET);
|
||||||
}
|
}
|
||||||
|
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.QUERY)) {
|
||||||
|
return requestMethodConditionCache.get(HttpMethod.QUERY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -184,6 +187,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
|
||||||
else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
|
else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
else if (this.methods.contains(RequestMethod.QUERY) && other.methods.contains(RequestMethod.HEAD)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,8 +189,9 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
|
||||||
HttpMethod httpMethod = request.getMethod();
|
HttpMethod httpMethod = request.getMethod();
|
||||||
Set<HttpMethod> methods = helper.getAllowedMethods();
|
Set<HttpMethod> methods = helper.getAllowedMethods();
|
||||||
if (HttpMethod.OPTIONS.equals(httpMethod)) {
|
if (HttpMethod.OPTIONS.equals(httpMethod)) {
|
||||||
Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
|
Set<MediaType> patchMediaTypes = helper.getConsumablePatchMediaTypes();
|
||||||
HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
|
Set<MediaType> queryMediaTypes = helper.getConsumableQueryMediaTypes();
|
||||||
|
HttpOptionsHandler handler = new HttpOptionsHandler(methods, patchMediaTypes, queryMediaTypes);
|
||||||
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
|
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
|
||||||
}
|
}
|
||||||
throw new MethodNotAllowedException(httpMethod, methods);
|
throw new MethodNotAllowedException(httpMethod, methods);
|
||||||
|
@ -323,14 +324,23 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
|
||||||
* PATCH specified, or that have no methods at all.
|
* PATCH specified, or that have no methods at all.
|
||||||
*/
|
*/
|
||||||
public Set<MediaType> getConsumablePatchMediaTypes() {
|
public Set<MediaType> getConsumablePatchMediaTypes() {
|
||||||
Set<MediaType> result = new LinkedHashSet<>();
|
return getConsumableMediaTypesForMethod(RequestMethod.PATCH);
|
||||||
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 declared "consumable" types but only among those that have
|
||||||
}
|
* PATCH specified, or that have no methods at all.
|
||||||
}
|
*/
|
||||||
return result;
|
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();
|
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.setAllow(initAllowedHttpMethods(declaredMethods));
|
||||||
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
|
this.headers.setAcceptPatch(new ArrayList<>(acceptPatch));
|
||||||
|
this.headers.setAcceptQuery(new ArrayList<>(acceptQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<HttpMethod> initAllowedHttpMethods(Set<HttpMethod> declaredMethods) {
|
private static Set<HttpMethod> initAllowedHttpMethods(Set<HttpMethod> declaredMethods) {
|
||||||
|
@ -413,7 +424,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Set<HttpMethod> result = new LinkedHashSet<>(declaredMethods);
|
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.HEAD);
|
||||||
}
|
}
|
||||||
result.add(HttpMethod.OPTIONS);
|
result.add(HttpMethod.OPTIONS);
|
||||||
|
|
|
@ -56,7 +56,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||||
*/
|
*/
|
||||||
public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler implements HandlerResultHandler {
|
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);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -141,7 +141,7 @@ class ResourceHandlerFunctionTests {
|
||||||
Mono<ServerResponse> responseMono = this.handlerFunction.handle(request);
|
Mono<ServerResponse> responseMono = this.handlerFunction.handle(request);
|
||||||
Mono<Void> result = responseMono.flatMap(response -> {
|
Mono<Void> result = responseMono.flatMap(response -> {
|
||||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
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);
|
return response.writeTo(exchange, context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ class ResourceHandlerFunctionTests {
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
assertThat(mockResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
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();
|
StepVerifier.create(mockResponse.getBody()).expectComplete().verify();
|
||||||
}
|
}
|
||||||
|
|
|
@ -377,7 +377,7 @@ class RequestMappingInfoHandlerMappingTests {
|
||||||
.isEqualTo(Collections.singletonList(new MediaType("application", "xml"))));
|
.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));
|
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.options(requestURI));
|
||||||
HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||||
|
|
||||||
|
@ -395,9 +395,15 @@ class RequestMappingInfoHandlerMappingTests {
|
||||||
HttpHeaders headers = (HttpHeaders) value;
|
HttpHeaders headers = (HttpHeaders) value;
|
||||||
assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
|
assertThat(headers.getAllow()).hasSameElementsAs(allowedMethods);
|
||||||
|
|
||||||
if (acceptPatch != null && headers.getAllow().contains(HttpMethod.PATCH) ) {
|
if (acceptMediaType != null) {
|
||||||
assertThat(headers.getAcceptPatch()).containsExactly(acceptPatch);
|
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) {
|
private void testMediaTypeNotAcceptable(String url) {
|
||||||
|
|
|
@ -117,7 +117,7 @@ class GlobalCorsConfigIntegrationTests extends AbstractRequestMappingIntegration
|
||||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(entity.getHeaders().getAccessControlAllowOrigin()).isEqualTo("*");
|
assertThat(entity.getHeaders().getAccessControlAllowOrigin()).isEqualTo("*");
|
||||||
assertThat(entity.getHeaders().getAccessControlAllowMethods())
|
assertThat(entity.getHeaders().getAccessControlAllowMethods())
|
||||||
.containsExactly(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST);
|
.containsExactly(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD, HttpMethod.POST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedHttpServerTest
|
@ParameterizedHttpServerTest
|
||||||
|
|
|
@ -43,7 +43,7 @@ import org.springframework.web.servlet.ModelAndView;
|
||||||
*/
|
*/
|
||||||
abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
|
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;
|
private final HttpStatusCode statusCode;
|
||||||
|
|
||||||
|
|
|
@ -265,6 +265,18 @@ public abstract class RequestPredicates {
|
||||||
return method(HttpMethod.OPTIONS).and(path(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.
|
* 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
|
* @param extension the path extension to match against, ignoring case
|
||||||
|
|
|
@ -41,7 +41,7 @@ import org.springframework.http.HttpStatus;
|
||||||
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
||||||
|
|
||||||
private static final Set<HttpMethod> SUPPORTED_METHODS =
|
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;
|
private final Resource resource;
|
||||||
|
@ -63,6 +63,11 @@ class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
||||||
.headers(headers -> this.headersConsumer.accept(this.resource, headers))
|
.headers(headers -> this.headersConsumer.accept(this.resource, headers))
|
||||||
.build();
|
.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)) {
|
else if (HttpMethod.HEAD.equals(method)) {
|
||||||
Resource headResource = new HeadMethodResource(this.resource);
|
Resource headResource = new HeadMethodResource(this.resource);
|
||||||
return EntityResponse.fromObject(headResource)
|
return EntityResponse.fromObject(headResource)
|
||||||
|
|
|
@ -229,6 +229,30 @@ class RouterFunctionBuilder implements RouterFunctions.Builder {
|
||||||
return add(RequestPredicates.OPTIONS(pattern).and(predicate), handlerFunction);
|
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
|
// other
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -610,6 +610,57 @@ public abstract class RouterFunctions {
|
||||||
*/
|
*/
|
||||||
Builder OPTIONS(String pattern, RequestPredicate predicate, HandlerFunction<ServerResponse> handlerFunction);
|
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<ServerResponse> 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
|
* Adds a route to the given handler function that handles all requests that match the
|
||||||
* given predicate.
|
* given predicate.
|
||||||
|
|
|
@ -162,6 +162,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
|
||||||
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
|
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
|
||||||
return requestMethodConditionCache.get(HttpMethod.GET.name());
|
return requestMethodConditionCache.get(HttpMethod.GET.name());
|
||||||
}
|
}
|
||||||
|
if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.QUERY)) {
|
||||||
|
return requestMethodConditionCache.get(HttpMethod.QUERY.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -189,6 +192,9 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi
|
||||||
else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
|
else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
else if (this.methods.contains(RequestMethod.QUERY) && other.methods.contains(RequestMethod.HEAD)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -519,7 +519,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
|
||||||
for (String method : declaredMethods) {
|
for (String method : declaredMethods) {
|
||||||
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
||||||
result.add(httpMethod);
|
result.add(httpMethod);
|
||||||
if (httpMethod == HttpMethod.GET) {
|
if (httpMethod == HttpMethod.GET || httpMethod == HttpMethod.QUERY) {
|
||||||
result.add(HttpMethod.HEAD);
|
result.add(HttpMethod.HEAD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
|
||||||
outputMessage.getServletResponse().setStatus(returnStatus.value());
|
outputMessage.getServletResponse().setStatus(returnStatus.value());
|
||||||
if (returnStatus.value() == HttpStatus.OK.value()) {
|
if (returnStatus.value() == HttpStatus.OK.value()) {
|
||||||
HttpMethod method = inputMessage.getMethod();
|
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)) {
|
isResourceNotModified(inputMessage, outputMessage)) {
|
||||||
outputMessage.flush();
|
outputMessage.flush();
|
||||||
return;
|
return;
|
||||||
|
@ -292,7 +292,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
|
||||||
HttpHeaders responseHeaders = response.getHeaders();
|
HttpHeaders responseHeaders = response.getHeaders();
|
||||||
String etag = responseHeaders.getETag();
|
String etag = responseHeaders.getETag();
|
||||||
long lastModifiedTimestamp = responseHeaders.getLastModified();
|
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.ETAG);
|
||||||
responseHeaders.remove(HttpHeaders.LAST_MODIFIED);
|
responseHeaders.remove(HttpHeaders.LAST_MODIFIED);
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||||
|
|
||||||
|
|
||||||
public ResourceHttpRequestHandler() {
|
public ResourceHttpRequestHandler() {
|
||||||
super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
|
super(HttpMethod.GET.name(), HttpMethod.QUERY.name(), HttpMethod.HEAD.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -931,7 +931,7 @@ public class MvcNamespaceTests {
|
||||||
CorsConfiguration config = configs.get("/**");
|
CorsConfiguration config = configs.get("/**");
|
||||||
assertThat(config).isNotNull();
|
assertThat(config).isNotNull();
|
||||||
assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"*"});
|
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.getAllowedHeaders().toArray()).isEqualTo(new String[]{"*"});
|
||||||
assertThat(config.getExposedHeaders()).isNull();
|
assertThat(config.getExposedHeaders()).isNull();
|
||||||
assertThat(config.getAllowCredentials()).isNull();
|
assertThat(config.getAllowCredentials()).isNull();
|
||||||
|
@ -964,7 +964,7 @@ public class MvcNamespaceTests {
|
||||||
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
|
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
|
||||||
config = configs.get("/resources/**");
|
config = configs.get("/resources/**");
|
||||||
assertThat(config.getAllowedOrigins().toArray()).isEqualTo(new String[]{"https://domain1.com"});
|
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.getAllowedHeaders().toArray()).isEqualTo(new String[]{"*"});
|
||||||
assertThat(config.getExposedHeaders()).isNull();
|
assertThat(config.getExposedHeaders()).isNull();
|
||||||
assertThat(config.getAllowCredentials()).isNull();
|
assertThat(config.getAllowCredentials()).isNull();
|
||||||
|
|
|
@ -175,7 +175,7 @@ class ResourceHandlerFunctionTests {
|
||||||
|
|
||||||
ServerResponse response = this.handlerFunction.handle(request);
|
ServerResponse response = this.handlerFunction.handle(request);
|
||||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
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();
|
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||||
ModelAndView mav = response.writeTo(servletRequest, servletResponse, this.context);
|
ModelAndView mav = response.writeTo(servletRequest, servletResponse, this.context);
|
||||||
|
@ -184,7 +184,7 @@ class ResourceHandlerFunctionTests {
|
||||||
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
assertThat(servletResponse.getStatus()).isEqualTo(200);
|
||||||
String allowHeader = servletResponse.getHeader("Allow");
|
String allowHeader = servletResponse.getHeader("Allow");
|
||||||
String[] methods = StringUtils.tokenizeToStringArray(allowHeader, ",");
|
String[] methods = StringUtils.tokenizeToStringArray(allowHeader, ",");
|
||||||
assertThat(methods).containsExactlyInAnyOrder("GET","HEAD","OPTIONS");
|
assertThat(methods).containsExactlyInAnyOrder("GET","QUERY","HEAD","OPTIONS");
|
||||||
byte[] actualBytes = servletResponse.getContentAsByteArray();
|
byte[] actualBytes = servletResponse.getContentAsByteArray();
|
||||||
assertThat(actualBytes).isEmpty();
|
assertThat(actualBytes).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,7 @@ public class HandlerMethodMappingTests {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain.com");
|
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
|
@Test
|
||||||
|
|
|
@ -193,9 +193,10 @@ class RequestMappingInfoHandlerMappingTests {
|
||||||
void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception {
|
void getHandlerHttpOptions(TestRequestMappingInfoHandlerMapping mapping) throws Exception {
|
||||||
testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS", null);
|
testHttpOptions(mapping, "/foo", "GET,HEAD,OPTIONS", null);
|
||||||
testHttpOptions(mapping, "/person/1", "PUT,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, "/something", "PUT,POST", null);
|
||||||
testHttpOptions(mapping, "/qux", "PATCH,GET,HEAD,OPTIONS", new MediaType("foo", "bar"));
|
testHttpOptions(mapping, "/qux", "PATCH,GET,HEAD,OPTIONS", new MediaType("foo", "bar"));
|
||||||
|
testHttpOptions(mapping, "/quid", "QUERY,HEAD,OPTIONS", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathPatternsParameterizedTest
|
@PathPatternsParameterizedTest
|
||||||
|
@ -572,6 +573,11 @@ class RequestMappingInfoHandlerMappingTests {
|
||||||
@RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar")
|
@RequestMapping(value = "/qux", method = RequestMethod.PATCH, consumes = "foo/bar")
|
||||||
public void patchBaz(String value) {
|
public void patchBaz(String value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/quid", method = RequestMethod.QUERY, consumes = "application/json", produces = "application/json")
|
||||||
|
public String query(@RequestBody String body) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,23 @@ class ResponseEntityExceptionHandlerTests {
|
||||||
assertThat(headers.getFirst(HttpHeaders.ACCEPT_PATCH)).isEqualTo("application/atom+xml, application/xml");
|
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
|
@Test
|
||||||
void httpMediaTypeNotAcceptable() {
|
void httpMediaTypeNotAcceptable() {
|
||||||
testException(new HttpMediaTypeNotAcceptableException(""));
|
testException(new HttpMediaTypeNotAcceptableException(""));
|
||||||
|
|
|
@ -114,7 +114,7 @@ class ResourceHttpRequestHandlerTests {
|
||||||
this.handler.handleRequest(this.request, this.response);
|
this.handler.handleRequest(this.request, this.response);
|
||||||
|
|
||||||
assertThat(this.response.getStatus()).isEqualTo(200);
|
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
|
@Test
|
||||||
|
|
|
@ -39,7 +39,7 @@ class WebContentGeneratorTests {
|
||||||
@Test
|
@Test
|
||||||
void getAllowHeaderWithConstructorFalse() {
|
void getAllowHeaderWithConstructorFalse() {
|
||||||
WebContentGenerator generator = new TestWebContentGenerator(false);
|
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
|
@Test
|
||||||
|
@ -59,7 +59,7 @@ class WebContentGeneratorTests {
|
||||||
void getAllowHeaderWithSupportedMethodsSetterEmpty() {
|
void getAllowHeaderWithSupportedMethodsSetterEmpty() {
|
||||||
WebContentGenerator generator = new TestWebContentGenerator();
|
WebContentGenerator generator = new TestWebContentGenerator();
|
||||||
generator.setSupportedMethods();
|
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
|
@Test
|
||||||
|
|
Loading…
Reference in New Issue