Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QUERY HTTP method #33430

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import static org.assertj.core.api.Assertions.entry;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.QUERY;

/**
* Tests for building a {@link MockHttpServletRequest} with
Expand Down Expand Up @@ -395,13 +396,15 @@ void requestParameterFromRequestBodyFormData() {
String contentType = "application/x-www-form-urlencoded;charset=UTF-8";
String body = "name+1=value+1&name+2=value+A&name+2=value+B&name+3";

MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST).uri("/foo")
.contentType(contentType).content(body.getBytes(UTF_8))
.buildRequest(this.servletContext);
for (HttpMethod method : List.of(POST, QUERY)) {
MockHttpServletRequest request = new MockHttpServletRequestBuilder(method).uri("/foo")
.contentType(contentType).content(body.getBytes(UTF_8))
.buildRequest(this.servletContext);

assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1");
assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B");
assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null);
assertThat(request.getParameterMap().get("name 1")).containsExactly("value 1");
assertThat(request.getParameterMap().get("name 2")).containsExactly("value A", "value B");
assertThat(request.getParameterMap().get("name 3")).containsExactly((String) null);
}
}

@Test
Expand Down
25 changes: 16 additions & 9 deletions spring-web/src/main/java/org/springframework/http/HttpMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,25 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {

/**
* The HTTP method {@code GET}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3">HTTP 1.1, section 9.3</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.1">HTTP Semantics, section 9.3.1</a>
*/
public static final HttpMethod GET = new HttpMethod("GET");

/**
* The HTTP method {@code HEAD}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4">HTTP 1.1, section 9.4</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.2">HTTP Semantics, section 9.3.2</a>
*/
public static final HttpMethod HEAD = new HttpMethod("HEAD");

/**
* The HTTP method {@code POST}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5">HTTP 1.1, section 9.5</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.3">HTTP Semantics, section 9.3.3</a>
*/
public static final HttpMethod POST = new HttpMethod("POST");

/**
* The HTTP method {@code PUT}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6">HTTP 1.1, section 9.6</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.4">HTTP Semantics, section 9.3.4</a>
*/
public static final HttpMethod PUT = new HttpMethod("PUT");

Expand All @@ -66,23 +66,29 @@ public final class HttpMethod implements Comparable<HttpMethod>, Serializable {

/**
* The HTTP method {@code DELETE}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7">HTTP 1.1, section 9.7</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.5">HTTP Semantics, section 9.3.5</a>
*/
public static final HttpMethod DELETE = new HttpMethod("DELETE");

/**
* The HTTP method {@code OPTIONS}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2">HTTP 1.1, section 9.2</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.7">HTTP Semantics, section 9.3.7</a>
*/
public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");

/**
* The HTTP method {@code TRACE}.
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8">HTTP 1.1, section 9.8</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.8">HTTP Semantics, section 9.3.8</a>
*/
public static final HttpMethod TRACE = new HttpMethod("TRACE");

private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE };
/**
* The HTTP method {@code QUERY}.
* @see <a href="https://httpwg.org/http-extensions/draft-ietf-httpbis-safe-method-w-body.html">IETF Draft</a>
*/
public static final HttpMethod QUERY = new HttpMethod("QUERY");

private static final HttpMethod[] values = new HttpMethod[] { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY };


private final String name;
Expand All @@ -96,7 +102,7 @@ private HttpMethod(String name) {
* Returns an array containing the standard HTTP methods. Specifically,
* this method returns an array containing {@link #GET}, {@link #HEAD},
* {@link #POST}, {@link #PUT}, {@link #PATCH}, {@link #DELETE},
* {@link #OPTIONS}, and {@link #TRACE}.
* {@link #OPTIONS}, {@link #TRACE}, and {@link #QUERY}.
*
* <p>Note that the returned value does not include any HTTP methods defined
* in WebDav.
Expand All @@ -123,6 +129,7 @@ public static HttpMethod valueOf(String method) {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
case "QUERY" -> QUERY;
default -> new HttpMethod(method);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,26 @@ public static BodyBuilder post(String uriTemplate, Object... uriVariables) {
return method(HttpMethod.POST, uriTemplate, uriVariables);
}

/**
* Create an HTTP QUERY builder with the given url.
* @param url the URL
* @return the created builder
*/
public static BodyBuilder query(URI url) {
return method(HttpMethod.QUERY, url);
}

/**
* Create an HTTP QUERY builder with the given string base uri template.
* @param uriTemplate the uri template to use
* @param uriVariables variables to expand the URI template with
* @return the created builder
* @since 6.2
*/
public static BodyBuilder query(String uriTemplate, Object... uriVariables) {
return method(HttpMethod.QUERY, uriTemplate, uriVariables);
}

/**
* Create an HTTP PUT builder with the given url.
* @param url the URL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpTrace;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
Expand Down Expand Up @@ -343,6 +344,9 @@ else if (HttpMethod.OPTIONS.equals(httpMethod)) {
else if (HttpMethod.TRACE.equals(httpMethod)) {
return new HttpTrace(uri);
}
else if (HttpMethod.QUERY.equals(httpMethod)) {
return new HttpUriRequestBase(HttpMethod.QUERY.name(), uri);
}
throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* @see PostMapping
* @see PutMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* @see PostMapping
* @see PutMapping
* @see DeleteMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* @see PostMapping
* @see DeleteMapping
* @see PatchMapping
* @see QueryMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

/**
* Annotation for mapping HTTP {@code QUERY} requests onto specific handler
* methods.
*
* <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 6.2
* @see GetMapping
* @see PutMapping
* @see PostMapping
* @see DeleteMapping
* @see PatchMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.QUERY)
public @interface QueryMapping {

/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";

/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};

/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};

/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};

/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};

/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};

/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
* at the method level. In most cases, at the method level applications will
* prefer to use one of the HTTP method specific variants
* {@link GetMapping @GetMapping}, {@link PostMapping @PostMapping},
* {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping}, or
* {@link PatchMapping @PatchMapping}.
* {@link PutMapping @PutMapping}, {@link DeleteMapping @DeleteMapping},
* {@link PatchMapping @PatchMapping}, or {@link QueryMapping}.
*
* <p><strong>NOTE:</strong> This annotation cannot be used in conjunction with
* other {@code @RequestMapping} annotations that are declared on the same element
Expand Down Expand Up @@ -121,7 +121,7 @@

/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE, QUERY.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit this
* HTTP method restriction.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
*
* <p>Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
* supports GET, HEAD, POST, PUT, PATCH, and DELETE only. DispatcherServlet will
* supports GET, QUERY, HEAD, POST, PUT, PATCH, and DELETE only. DispatcherServlet will
* process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
* told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
* and "dispatchTraceRequest" properties, switching them to "true" if necessary.
Expand All @@ -38,7 +38,7 @@
*/
public enum RequestMethod {

GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, QUERY;


/**
Expand All @@ -60,6 +60,7 @@ public static RequestMethod resolve(String method) {
case "DELETE" -> DELETE;
case "OPTIONS" -> OPTIONS;
case "TRACE" -> TRACE;
case "QUERY" -> QUERY;
default -> null;
};
}
Expand Down Expand Up @@ -93,6 +94,7 @@ public HttpMethod asHttpMethod() {
case DELETE -> HttpMethod.DELETE;
case OPTIONS -> HttpMethod.OPTIONS;
case TRACE -> HttpMethod.TRACE;
case QUERY -> HttpMethod.QUERY;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ public RequestHeadersUriSpec<?> options() {
return methodInternal(HttpMethod.OPTIONS);
}

@Override
public RequestHeadersUriSpec<?> query() {
return methodInternal(HttpMethod.QUERY);
}

@Override
public RequestBodyUriSpec method(HttpMethod method) {
Assert.notNull(method, "HttpMethod must not be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ public interface RestClient {
*/
RequestHeadersUriSpec<?> options();

/**
* Start building an HTTP QUERY request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> query();

/**
* Start building a request for the given {@code HttpMethod}.
* @return a spec for specifying the target URL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ public class CorsConfiguration {

private static final List<String> DEFAULT_PERMIT_ALL = Collections.singletonList(ALL);

private static final List<HttpMethod> DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.HEAD);
private static final List<HttpMethod> DEFAULT_METHODS = List.of(HttpMethod.GET, HttpMethod.QUERY, HttpMethod.HEAD);

private static final List<String> DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(),
private static final List<String> DEFAULT_PERMIT_METHODS = List.of(HttpMethod.GET.name(), HttpMethod.QUERY.name(),
HttpMethod.HEAD.name(), HttpMethod.POST.name());


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletRespo

if (!response.isCommitted() &&
responseStatusCode >= 200 && responseStatusCode < 300 &&
HttpMethod.GET.matches(request.getMethod())) {
(HttpMethod.GET.matches(request.getMethod()) || HttpMethod.QUERY.matches(request.getMethod()))) {

String cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL);
return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE));
Expand Down
Loading