From f6fbdafb6a5b364bc2538b4f05a85fcc9be9fc51 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 7 May 2014 10:39:46 +0200 Subject: [PATCH] Introduce RequestEntity and builder This commit introduces the RequestEntity, a class similar to ResponseEntity, but meant for HTTP requests rather than responses. The RequestEntity can be used both in RestTemplate as well as @MVC scenarios. The class also comes with a builder, similar to the one found in ResponseEntity, which allows for building of a RequestEntity through a fluent API. Issue: SPR-11752 --- .../springframework/http/RequestEntity.java | 597 ++++++++++++++++++ .../web/client/RestOperations.java | 41 ++ .../web/client/RestTemplate.java | 23 + .../http/RequestEntityTests.java | 147 +++++ .../annotation/HttpEntityMethodProcessor.java | 18 +- .../HttpEntityMethodProcessorMockTests.java | 36 +- 6 files changed, 856 insertions(+), 6 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/RequestEntity.java create mode 100644 spring-web/src/test/java/org/springframework/http/RequestEntityTests.java diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java new file mode 100644 index 0000000000..592508b74f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -0,0 +1,597 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.http; + +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Map; + +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.web.util.UriTemplate; + +/** + * Extension of {@link HttpEntity} that adds a {@linkplain HttpMethod method} and + * {@linkplain URI uri}. + * Used in {@code RestTemplate} as well {@code @Controller} methods. + * + *

In {@code RestTemplate}, this class is used as parameter in + * {@link org.springframework.web.client.RestTemplate#exchange(RequestEntity, Class) exchange()}: + *

+ * MyRequest body = ...
+ * RequestEntity<MyRequest> request = RequestEntity.post("http://example.com/{foo}", "bar").accept(MediaType.APPLICATION_JSON).body(body);
+ * ResponseEntity<MyResponse> response = template.exchange(request, MyResponse.class);
+ * 
+ * + *

Can also be used in Spring MVC, as a parameter in a @Controller method: + *

+ * @RequestMapping("/handle")
+ * public void handle(RequestEntity<String> request) {
+ *   HttpMethod method = request.getMethod();
+ *   URI url = request.getUrl();
+ *   String body = request.getBody();
+ * }
+ * 
+ * + * @author Arjen Poutsma + * @since 4.1 + * @see #getMethod() + * @see #getUrl() + */ +public class RequestEntity extends HttpEntity { + + private final HttpMethod method; + + private final URI url; + + /** + * Create a new {@code RequestEntity} with the given method and URL, and no body nor headers. + * @param method the method + * @param url the URL + */ + public RequestEntity(HttpMethod method, URI url) { + super(); + this.method = method; + this.url = url; + } + + /** + * Create a new {@code RequestEntity} with the given method, URL, body, and no headers. + * @param body the body + * @param method the method + * @param url the URL + */ + public RequestEntity(T body, HttpMethod method, URI url) { + super(body); + this.method = method; + this.url = url; + } + + /** + * Create a new {@code RequestEntity} with the given method, URL, body, headers and no + * body + * @param headers the headers + * @param method the method + * @param url the URL + */ + public RequestEntity(MultiValueMap headers, HttpMethod method, URI url) { + super(headers); + this.method = method; + this.url = url; + } + + /** + * Create a new {@code RequestEntity} with the given method, URL, body, headers and + * body + * @param body the body + * @param headers the headers + * @param method the method + * @param url the URL + */ + public RequestEntity(T body, MultiValueMap headers, + HttpMethod method, URI url) { + super(body, headers); + this.method = method; + this.url = url; + } + + /** + * Return the HTTP method of the request. + * @return the HTTP method as an {@code HttpMethod} enum value + */ + public HttpMethod getMethod() { + return method; + } + + /** + * Return the URL of the request. + * @return the URL as a {@code URI} + */ + public URI getUrl() { + return url; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof RequestEntity)) { + return false; + } + RequestEntity otherEntity = (RequestEntity) other; + return (ObjectUtils.nullSafeEquals(this.method, otherEntity.method) && + ObjectUtils.nullSafeEquals(this.url, otherEntity.url) && + super.equals(other)); + } + + @Override + public int hashCode() { + return 29 * super.hashCode() + + 29 * ObjectUtils.nullSafeHashCode(this.method) + + ObjectUtils.nullSafeHashCode(this.url); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("<"); + builder.append(this.method.toString()); + builder.append(' '); + builder.append(this.url); + builder.append(','); + T body = getBody(); + HttpHeaders headers = getHeaders(); + if (body != null) { + builder.append(body); + if (headers != null) { + builder.append(','); + } + } + if (headers != null) { + builder.append(headers); + } + builder.append('>'); + return builder.toString(); + } + + // Static builder methods + + /** + * Creates a builder with the given method, url, and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param method the HTTP method (GET, POST, etc) + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, String url, + Object... uriVariables) { + URI expanded = new UriTemplate(url).expand(uriVariables); + return new DefaultBodyBuilder(method, expanded); + } + + /** + * Creates a builder with the given method, url, and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param method the HTTP method (GET, POST, etc) + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, String url, + Map uriVariables) { + URI expanded = new UriTemplate(url).expand(uriVariables); + return new DefaultBodyBuilder(method, expanded); + } + + /** + * Creates a builder with the given method, url, and uri variables. + * @param method the HTTP method (GET, POST, etc) + * @param url the URL + * @return the created builder + */ + public static BodyBuilder method(HttpMethod method, URI url) { + return new DefaultBodyBuilder(method, url); + } + + /** + * Creates a GET builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder get(String url, Object... uriVariables) { + return method(HttpMethod.GET, url, uriVariables); + } + + /** + * Creates a GET builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder get(String url, Map uriVariables) { + return method(HttpMethod.GET, url, uriVariables); + } + + /** + * Creates a GET builder with the given url. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @return the created builder + */ + public static HeadersBuilder get(URI url) { + return method(HttpMethod.GET, url); + } + + /** + * Creates a HEAD builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder head(String url, Object... uriVariables) { + return method(HttpMethod.HEAD, url, uriVariables); + } + + /** + * Creates a HEAD builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder head(String url, Map uriVariables) { + return method(HttpMethod.HEAD, url, uriVariables); + } + + /** + * Creates a HEAD builder with the given url. + * @param url the URL + * @return the created builder + */ + public static HeadersBuilder head(URI url) { + return method(HttpMethod.HEAD, url); + } + + /** + * Creates a POST builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder post(String url, Object... uriVariables) { + return method(HttpMethod.POST, url, uriVariables); + } + + /** + * Creates a POST builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder post(String url, Map uriVariables) { + return method(HttpMethod.POST, url, uriVariables); + } + + /** + * Creates a POST builder with the given url. + * @param url the URL + * @return the created builder + */ + public static BodyBuilder post(URI url) { + return method(HttpMethod.POST, url); + } + + /** + * Creates a PUT builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder put(String url, + Object... uriVariables) { + return method(HttpMethod.PUT, url, uriVariables); + } + + /** + * Creates a PUT builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder put(String url, + Map uriVariables) { + return method(HttpMethod.PUT, url, uriVariables); + } + + /** + * Creates a PUT builder with the given url. + * @param url the URL + * @return the created builder + */ + public static BodyBuilder put(URI url) { + return method(HttpMethod.PUT, url); + } + + /** + * Creates a PATCH builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder patch(String url, + Object... uriVariables) { + return method(HttpMethod.PATCH, url, uriVariables); + } + + /** + * Creates a PATCH builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static BodyBuilder patch(String url, + Map uriVariables) { + return method(HttpMethod.PATCH, url, uriVariables); + } + + /** + * Creates a PATCH builder with the given url. + * @param url the URL + * @return the created builder + */ + public static BodyBuilder patch(URI url) { + return method(HttpMethod.PATCH, url); + } + + /** + * Creates a DELETE builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder delete(String url, + Object... uriVariables) { + return method(HttpMethod.DELETE, url, uriVariables); + } + + /** + * Creates a DELETE builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder delete(String url, + Map uriVariables) { + return method(HttpMethod.DELETE, url, uriVariables); + } + + /** + * Creates a DELETE builder with the given url. + * @param url the URL + * @return the created builder + */ + public static HeadersBuilder delete(URI url) { + return method(HttpMethod.DELETE, url); + } + + /** + * Creates an OPTIONS builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder options(String url, + Object... uriVariables) { + return method(HttpMethod.OPTIONS, url, uriVariables); + } + + /** + * Creates an OPTIONS builder with the given url and uri variables. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the created builder + */ + public static HeadersBuilder options(String url, + Map uriVariables) { + return method(HttpMethod.OPTIONS, url, uriVariables); + } + + /** + * Creates an OPTIONS builder with the given url. + * @param url the URL + * @return the created builder + */ + public static HeadersBuilder options(URI url) { + return method(HttpMethod.OPTIONS, url); + } + + /** + * Defines a builder that adds headers to the request entity. + * @param the builder subclass + */ + public interface HeadersBuilder> { + + /** + * Add the given, single header value under the given name. + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + * @see HttpHeaders#add(String, String) + */ + B header(String headerName, String... headerValues); + + /** + * Set the list of acceptable {@linkplain MediaType media types}, as specified + * by the {@code Accept} header. + * @param acceptableMediaTypes the acceptable media types + */ + B accept(MediaType... acceptableMediaTypes); + + /** + * Set the list of acceptable {@linkplain Charset charsets}, as specified by + * the {@code Accept-Charset} header. + * @param acceptableCharsets the acceptable charsets + */ + B acceptCharset(Charset... acceptableCharsets); + + /** + * Sets the value of the {@code If-Modified-Since} header. + *

The date should be specified as the number of milliseconds since January 1, + * 1970 GMT. + * @param ifModifiedSince the new value of the header + */ + B ifModifiedSince(long ifModifiedSince); + + /** + * Sets the values of the {@code If-None-Match} header. + * @param ifNoneMatches the new value of the header + */ + B ifNoneMatch(String... ifNoneMatches); + + /** + * Builds the request entity with no body. + * @return the request entity + * @see BodyBuilder#body(Object) + */ + RequestEntity build(); + + } + + /** + * Defines a builder that adds a body to the response entity. + */ + public interface BodyBuilder extends HeadersBuilder { + + /** + * Set the length of the body in bytes, as specified by the {@code Content-Length} + * header. + * @param contentLength the content length + * @return this builder + * @see HttpHeaders#setContentLength(long) + */ + BodyBuilder contentLength(long contentLength); + + /** + * Set the {@linkplain MediaType media type} of the body, as specified by the + * {@code Content-Type} header. + * @param contentType the content type + * @return this builder + * @see HttpHeaders#setContentType(MediaType) + */ + BodyBuilder contentType(MediaType contentType); + + /** + * Sets the body of the request entity and returns it. + * @param body the body of the request entity + * @param the type of the body + * @return the built request entity + */ + RequestEntity body(T body); + + } + + private static class DefaultBodyBuilder implements BodyBuilder { + + private final HttpMethod method; + + private final URI url; + + private final HttpHeaders headers = new HttpHeaders(); + + + public DefaultBodyBuilder(HttpMethod method, URI url) { + this.method = method; + this.url = url; + } + + @Override + public BodyBuilder header(String headerName, String... headerValues) { + for (String headerValue : headerValues) { + this.headers.add(headerName, headerValue); + } + return this; + } + + @Override + public BodyBuilder accept(MediaType... acceptableMediaTypes) { + this.headers.setAccept(Arrays.asList(acceptableMediaTypes)); + return this; + } + + @Override + public BodyBuilder acceptCharset(Charset... acceptableCharsets) { + this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets)); + return this; + } + + @Override + public BodyBuilder contentLength(long contentLength) { + this.headers.setContentLength(contentLength); + return this; + } + + @Override + public BodyBuilder contentType(MediaType contentType) { + this.headers.setContentType(contentType); + return this; + } + + @Override + public BodyBuilder ifModifiedSince(long ifModifiedSince) { + this.headers.setIfModifiedSince(ifModifiedSince); + return this; + } + + @Override + public BodyBuilder ifNoneMatch(String... ifNoneMatches) { + this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches)); + return this; + } + + @Override + public RequestEntity build() { + return new RequestEntity(null, this.headers, this.method, this.url); + } + + @Override + public RequestEntity body(T body) { + return new RequestEntity(body, this.headers, this.method, this.url); + } + } + + + + + + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index 1f758ff212..6a0a291e88 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -24,6 +24,7 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; /** @@ -461,6 +462,46 @@ public interface RestOperations { ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, ParameterizedTypeReference responseType) throws RestClientException; + /** + * Execute the HTTP method and URL of the {@link RequestEntity}, writing it to the + * request, and returns the response as {@link ResponseEntity}. Typically used in + * combination with the static builder methods on {@code RequestEntity}, for instance: + * + *

+	 * MyRequest body = ...
+	 * RequestEntity request = RequestEntity.post("http://example.com/{foo}", "bar").accept(MediaType.APPLICATION_JSON).body(body);
+	 * ResponseEntity<MyResponse> response = template.exchange(request, MyResponse.class);
+	 * 
+ * + * @param requestEntity the entity to write to the request + * @param responseType the type of the return value + * @return the response as entity + * @since 4.1 + */ + ResponseEntity exchange(RequestEntity requestEntity, + Class responseType) throws RestClientException; + + /** + * Execute the HTTP method and URL of the {@link RequestEntity}, writing it to the + * request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type information: + * + *
+	 * MyRequest body = ...
+	 * RequestEntity request = RequestEntity.post("http://example.com/{foo}", "bar").accept(MediaType.APPLICATION_JSON).body(body);
+	 * ParameterizedTypeReference<List<MyResponse>> myBean = new ParameterizedTypeReference<List<MyResponse>>() {};
+	 * ResponseEntity<List<MyResponse>> response = template.exchange(request, myBean);
+	 * 
+ * + * @param requestEntity the entity to write to the request + * @param responseType the type of the return value + * @return the response as entity + * @since 4.1 + */ + ResponseEntity exchange(RequestEntity requestEntity, + ParameterizedTypeReference responseType) throws RestClientException; + + // general execution /** diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 3f3eb939ff..ea75dffc3e 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; @@ -493,6 +494,28 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat return execute(url, method, requestCallback, responseExtractor); } + @Override + public ResponseEntity exchange(RequestEntity requestEntity, + Class responseType) throws RestClientException { + Assert.notNull(requestEntity, "'requestEntity' must not be null"); + + RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return execute(requestEntity.getUrl(), requestEntity.getMethod(), requestCallback, responseExtractor); + } + + @Override + public ResponseEntity exchange(RequestEntity requestEntity, + ParameterizedTypeReference responseType) throws RestClientException { + Assert.notNull(requestEntity, "'requestEntity' must not be null"); + + Type type = responseType.getType(); + RequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = responseEntityExtractor(type); + return execute(requestEntity.getUrl(), requestEntity.getMethod(), requestCallback, responseExtractor); + } + + // general execution @Override diff --git a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java new file mode 100644 index 0000000000..f475b4a82d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.http; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +public class RequestEntityTests { + + @Test + public void normal() throws URISyntaxException { + String headerName = "My-Custom-Header"; + String headerValue = "HeaderValue"; + URI url = new URI("http://example.com"); + Integer entity = 42; + + RequestEntity requestEntity = + RequestEntity.method(HttpMethod.GET, url) + .header(headerName, headerValue).body(entity); + + assertNotNull(requestEntity); + assertEquals(HttpMethod.GET, requestEntity.getMethod()); + assertTrue(requestEntity.getHeaders().containsKey(headerName)); + assertEquals(headerValue, requestEntity.getHeaders().getFirst(headerName)); + assertEquals(entity, requestEntity.getBody()); + } + + @Test + public void uriVariablesExpansion() throws URISyntaxException { + RequestEntity.get("http://example.com/{foo}", "bar").accept(MediaType.TEXT_PLAIN).build(); + + String url = "http://www.{host}.com/{path}"; + String host = "example"; + String path = "foo/bar"; + + URI expected = new URI("http://www.example.com/foo/bar"); + + RequestEntity entity = RequestEntity.method(HttpMethod.GET, url, host, path).build(); + assertEquals(expected, entity.getUrl()); + + Map uriVariables = new HashMap(2); + uriVariables.put("host", host); + uriVariables.put("path", path); + + entity = RequestEntity.method(HttpMethod.GET, url, uriVariables).build(); + assertEquals(expected, entity.getUrl()); + } + + @Test + public void get() { + RequestEntity requestEntity = RequestEntity.get(URI.create("http://example.com")).accept( + MediaType.IMAGE_GIF, MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG).build(); + + assertNotNull(requestEntity); + assertEquals(HttpMethod.GET, requestEntity.getMethod()); + assertTrue(requestEntity.getHeaders().containsKey("Accept")); + assertEquals("image/gif, image/jpeg, image/png", requestEntity.getHeaders().getFirst("Accept")); + assertNull(requestEntity.getBody()); + } + + @Test + public void headers() throws URISyntaxException { + MediaType accept = MediaType.TEXT_PLAIN; + Charset charset = Charset.forName("UTF-8"); + long ifModifiedSince = 12345L; + String ifNoneMatch = "\"foo\""; + long contentLength = 67890; + MediaType contentType = MediaType.TEXT_PLAIN; + + RequestEntity responseEntity = RequestEntity.post("http://example.com"). + accept(accept). + acceptCharset(charset). + ifModifiedSince(ifModifiedSince). + ifNoneMatch(ifNoneMatch). + contentLength(contentLength). + contentType(contentType). + build(); + + assertNotNull(responseEntity); + assertEquals(HttpMethod.POST, responseEntity.getMethod()); + assertEquals(new URI("http://example.com"), responseEntity.getUrl()); + HttpHeaders responseHeaders = responseEntity.getHeaders(); + + assertEquals("text/plain", responseHeaders.getFirst("Accept")); + assertEquals("utf-8", responseHeaders.getFirst("Accept-Charset")); + assertEquals("Thu, 01 Jan 1970 00:00:12 GMT", + responseHeaders.getFirst("If-Modified-Since")); + assertEquals(ifNoneMatch, responseHeaders.getFirst("If-None-Match")); + assertEquals(String.valueOf(contentLength), responseHeaders.getFirst("Content-Length")); + assertEquals(contentType.toString(), responseHeaders.getFirst("Content-Type")); + + assertNull(responseEntity.getBody()); + } + + @Test + public void methods() throws URISyntaxException { + URI url = new URI("http://example.com"); + + RequestEntity entity = RequestEntity.get(url).build(); + assertEquals(HttpMethod.GET, entity.getMethod()); + + entity = RequestEntity.post(url).build(); + assertEquals(HttpMethod.POST, entity.getMethod()); + + entity = RequestEntity.head(url).build(); + assertEquals(HttpMethod.HEAD, entity.getMethod()); + + entity = RequestEntity.options(url).build(); + assertEquals(HttpMethod.OPTIONS, entity.getMethod()); + + entity = RequestEntity.put(url).build(); + assertEquals(HttpMethod.PUT, entity.getMethod()); + + entity = RequestEntity.patch(url).build(); + assertEquals(HttpMethod.PATCH, entity.getMethod()); + + entity = RequestEntity.delete(url).build(); + assertEquals(HttpMethod.DELETE, entity.getMethod()); + + } + + + + +} \ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 25046f6c86..8374f32015 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -25,6 +25,7 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; @@ -68,12 +69,14 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro @Override public boolean supportsParameter(MethodParameter parameter) { - return HttpEntity.class.equals(parameter.getParameterType()); + return HttpEntity.class.equals(parameter.getParameterType()) || + RequestEntity.class.equals(parameter.getParameterType()); } @Override public boolean supportsReturnType(MethodParameter returnType) { - return HttpEntity.class.isAssignableFrom(returnType.getParameterType()); + return HttpEntity.class.equals(returnType.getParameterType()) || + ResponseEntity.class.equals(returnType.getParameterType()); } @Override @@ -81,11 +84,18 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws IOException, HttpMediaTypeNotSupportedException { - HttpInputMessage inputMessage = createInputMessage(webRequest); + ServletServerHttpRequest inputMessage = createInputMessage(webRequest); Type paramType = getHttpEntityType(parameter); Object body = readWithMessageConverters(webRequest, parameter, paramType); - return new HttpEntity(body, inputMessage.getHeaders()); + if (RequestEntity.class.equals(parameter.getParameterType())) { + return new RequestEntity(body, inputMessage.getHeaders(), + inputMessage.getMethod(), inputMessage.getURI()); + } + else { + return new HttpEntity(body, inputMessage.getHeaders()); + + } } private Type getHttpEntityType(MethodParameter parameter) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java index 0e032a08bb..7397465d4c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.Method; +import java.net.URI; import java.util.Arrays; import java.util.Collections; @@ -27,9 +28,11 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mock.web.test.MockHttpServletRequest; @@ -65,6 +68,7 @@ public class HttpEntityMethodProcessorMockTests { private MethodParameter returnTypeResponseEntity; private MethodParameter returnTypeHttpEntity; private MethodParameter returnTypeInt; + private MethodParameter paramRequestEntity; private MethodParameter returnTypeResponseEntityProduces; private ModelAndViewContainer mavContainer; @@ -75,6 +79,7 @@ public class HttpEntityMethodProcessorMockTests { private MockHttpServletRequest servletRequest; + @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { @@ -85,10 +90,11 @@ public class HttpEntityMethodProcessorMockTests { reset(messageConverter); - Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class, Integer.TYPE); + Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class, Integer.TYPE, RequestEntity.class); paramHttpEntity = new MethodParameter(handle1, 0); paramResponseEntity = new MethodParameter(handle1, 1); paramInt = new MethodParameter(handle1, 2); + paramRequestEntity = new MethodParameter(handle1, 3); returnTypeResponseEntity = new MethodParameter(handle1, -1); returnTypeHttpEntity = new MethodParameter(getClass().getMethod("handle2", HttpEntity.class), -1); @@ -107,6 +113,7 @@ public class HttpEntityMethodProcessorMockTests { @Test public void supportsParameter() { assertTrue("HttpEntity parameter not supported", processor.supportsParameter(paramHttpEntity)); + assertTrue("RequestEntity parameter not supported", processor.supportsParameter(paramRequestEntity)); assertFalse("ResponseEntity parameter supported", processor.supportsParameter(paramResponseEntity)); assertFalse("non-entity parameter supported", processor.supportsParameter(paramInt)); } @@ -115,6 +122,8 @@ public class HttpEntityMethodProcessorMockTests { public void supportsReturnType() { assertTrue("ResponseEntity return type not supported", processor.supportsReturnType(returnTypeResponseEntity)); assertTrue("HttpEntity return type not supported", processor.supportsReturnType(returnTypeHttpEntity)); + assertFalse("RequestEntity parameter supported", + processor.supportsReturnType(paramRequestEntity)); assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(returnTypeInt)); } @@ -134,6 +143,29 @@ public class HttpEntityMethodProcessorMockTests { assertEquals("Invalid argument", body, ((HttpEntity) result).getBody()); } + @Test + public void resolveArgumentRequestEntity() throws Exception { + MediaType contentType = MediaType.TEXT_PLAIN; + servletRequest.addHeader("Content-Type", contentType.toString()); + servletRequest.setMethod("GET"); + servletRequest.setServerName("www.example.com"); + servletRequest.setServerPort(80); + servletRequest.setRequestURI("/path"); + + String body = "Foo"; + given(messageConverter.canRead(String.class, contentType)).willReturn(true); + given(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body); + + Object result = processor.resolveArgument(paramRequestEntity, mavContainer, webRequest, null); + + assertTrue(result instanceof RequestEntity); + assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled()); + RequestEntity requestEntity = (RequestEntity) result; + assertEquals("Invalid method", HttpMethod.GET, requestEntity.getMethod()); + assertEquals("Invalid url", new URI("http", null, "www.example.com", 80, "/path", null, null), requestEntity.getUrl()); + assertEquals("Invalid argument", body, requestEntity.getBody()); + } + @Test(expected = HttpMediaTypeNotSupportedException.class) public void resolveArgumentNotReadable() throws Exception { MediaType contentType = MediaType.TEXT_PLAIN; @@ -262,7 +294,7 @@ public class HttpEntityMethodProcessorMockTests { assertEquals("headerValue", outputMessage.getValue().getHeaders().get("header").get(0)); } - public ResponseEntity handle1(HttpEntity httpEntity, ResponseEntity responseEntity, int i) { + public ResponseEntity handle1(HttpEntity httpEntity, ResponseEntity responseEntity, int i, RequestEntity requestEntity) { return responseEntity; }