summaryrefslogtreecommitdiff
path: root/spring-web/src
diff options
context:
space:
mode:
Diffstat (limited to 'spring-web/src')
-rw-r--r--spring-web/src/main/java/org/springframework/http/CacheControl.java45
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpHeaders.java238
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpMethod.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpRange.java49
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpStatus.java38
-rw-r--r--spring-web/src/main/java/org/springframework/http/MediaType.java87
-rw-r--r--spring-web/src/main/java/org/springframework/http/RequestEntity.java74
-rw-r--r--spring-web/src/main/java/org/springframework/http/ResponseEntity.java102
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java47
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java71
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java13
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java21
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java117
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java59
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java102
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java71
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java155
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java82
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java102
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java89
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java48
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java3
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java6
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java9
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java17
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java4
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java4
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java66
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java68
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java41
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java78
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java42
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java190
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java14
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java126
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java7
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java94
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java27
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java5
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java47
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java36
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java6
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java8
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java17
-rw-r--r--spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java7
-rw-r--r--spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java22
-rw-r--r--spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java28
-rw-r--r--spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java6
-rw-r--r--spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java25
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java16
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java15
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java72
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java26
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java48
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java10
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java90
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java88
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java24
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java90
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java90
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java90
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java66
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java16
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java6
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java14
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java103
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java74
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java7
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java173
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java79
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java109
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/RestTemplate.java65
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java69
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java64
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java64
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java64
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java14
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java52
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java123
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java40
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java3
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java3
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java25
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java92
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java225
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java44
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java7
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java47
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java78
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java20
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java67
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java12
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java142
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java11
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java22
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java47
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java6
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java199
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java30
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java138
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java148
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java21
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java9
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriTemplate.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java20
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriUtils.java27
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java10
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/WebUtils.java14
-rw-r--r--spring-web/src/test/java/org/springframework/http/CacheControlTests.java15
-rw-r--r--spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java74
-rw-r--r--spring-web/src/test/java/org/springframework/http/HttpRangeTests.java47
-rw-r--r--spring-web/src/test/java/org/springframework/http/HttpStatusTests.java3
-rw-r--r--spring-web/src/test/java/org/springframework/http/MediaTypeTests.java4
-rw-r--r--spring-web/src/test/java/org/springframework/http/RequestEntityTests.java15
-rw-r--r--spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java20
-rw-r--r--spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java15
-rw-r--r--spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java40
-rw-r--r--spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java40
-rw-r--r--spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java2
-rw-r--r--spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java128
-rw-r--r--spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java78
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java2
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java67
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java142
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java87
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java16
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java17
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java57
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java4
-rw-r--r--spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java12
-rw-r--r--spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java13
-rw-r--r--spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java4
-rw-r--r--spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java4
-rw-r--r--spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java11
-rw-r--r--spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java6
-rw-r--r--spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java18
-rw-r--r--spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java32
-rw-r--r--spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java34
-rw-r--r--spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java188
-rw-r--r--spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java70
-rw-r--r--spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java37
-rw-r--r--spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java16
-rw-r--r--spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java47
-rw-r--r--spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java10
-rw-r--r--spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java2
-rw-r--r--spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java16
-rw-r--r--spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java15
-rw-r--r--spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java6
-rw-r--r--spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java4
-rw-r--r--spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java24
-rw-r--r--spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java241
-rw-r--r--spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java43
-rw-r--r--spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java198
-rw-r--r--spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java180
-rw-r--r--spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java121
-rw-r--r--spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java105
-rw-r--r--spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java115
-rw-r--r--spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java6
-rw-r--r--spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java74
-rw-r--r--spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java21
-rw-r--r--spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt1
-rw-r--r--spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd10
180 files changed, 6915 insertions, 1631 deletions
diff --git a/spring-web/src/main/java/org/springframework/http/CacheControl.java b/spring-web/src/main/java/org/springframework/http/CacheControl.java
index a291e7ae..c9933344 100644
--- a/spring-web/src/main/java/org/springframework/http/CacheControl.java
+++ b/spring-web/src/main/java/org/springframework/http/CacheControl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -65,6 +65,10 @@ public class CacheControl {
private boolean proxyRevalidate = false;
+ private long staleWhileRevalidate = -1;
+
+ private long staleIfError = -1;
+
private long sMaxAge = -1;
@@ -110,7 +114,7 @@ public class CacheControl {
* clients sending conditional requests (with "ETag", "If-Modified-Since" headers) and the server responding
* with "304 - Not Modified" status.
* <p>In order to disable caching and minimize requests/responses exchanges, the {@link #noStore()} directive
- * should be used.
+ * should be used instead of {@link #noCache()}.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a>
*/
@@ -207,6 +211,36 @@ public class CacheControl {
return this;
}
+ /**
+ * Add a "stale-while-revalidate" directive.
+ * <p>This directive indicates that caches MAY serve the response in
+ * which it appears after it becomes stale, up to the indicated number of seconds.
+ * If a cached response is served stale due to the presence of this extension,
+ * the cache SHOULD attempt to revalidate it while still serving stale responses (i.e., without blocking).
+ * @param staleWhileRevalidate the maximum time the response should be used while being revalidated
+ * @param unit the time unit of the {@code staleWhileRevalidate} argument
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a>
+ */
+ public CacheControl staleWhileRevalidate(long staleWhileRevalidate, TimeUnit unit) {
+ this.staleWhileRevalidate = unit.toSeconds(staleWhileRevalidate);
+ return this;
+ }
+
+ /**
+ * Add a "stale-if-error" directive.
+ * <p>This directive indicates that when an error is encountered, a cached stale response MAY be used to satisfy
+ * the request, regardless of other freshness information.
+ * @param staleIfError the maximum time the response should be used when errors are encountered
+ * @param unit the time unit of the {@code staleIfError} argument
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a>
+ */
+ public CacheControl staleIfError(long staleIfError, TimeUnit unit) {
+ this.staleIfError = unit.toSeconds(staleIfError);
+ return this;
+ }
+
/**
* Return the "Cache-Control" header value.
@@ -241,6 +275,13 @@ public class CacheControl {
if (this.sMaxAge != -1) {
appendDirective(ccValue, "s-maxage=" + Long.toString(this.sMaxAge));
}
+ if (this.staleIfError != -1) {
+ appendDirective(ccValue, "stale-if-error=" + Long.toString(this.staleIfError));
+ }
+ if (this.staleWhileRevalidate != -1) {
+ appendDirective(ccValue, "stale-while-revalidate=" + Long.toString(this.staleWhileRevalidate));
+ }
+
String ccHeaderValue = ccValue.toString();
return (StringUtils.hasText(ccHeaderValue) ? ccHeaderValue : null);
}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
index 1170bb06..0ba9a02a 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
@@ -34,6 +34,8 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
@@ -55,6 +57,8 @@ import org.springframework.util.StringUtils;
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
+ * @author Brian Clozel
+ * @author Juergen Hoeller
* @since 3.0
*/
public class HttpHeaders implements MultiValueMap<String, String>, Serializable {
@@ -372,6 +376,12 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
"EEE MMM dd HH:mm:ss yyyy"
};
+ /**
+ * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match"
+ * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
+ */
+ private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
+
private static TimeZone GMT = TimeZone.getTimeZone("GMT");
@@ -419,19 +429,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* <p>Returns an empty list when the acceptable media types are unspecified.
*/
public List<MediaType> getAccept() {
- String value = getFirst(ACCEPT);
- List<MediaType> result = (value != null ? MediaType.parseMediaTypes(value) : Collections.<MediaType>emptyList());
-
- // Some containers parse 'Accept' into multiple values
- if (result.size() == 1) {
- List<String> acceptHeader = get(ACCEPT);
- if (acceptHeader.size() > 1) {
- value = StringUtils.collectionToCommaDelimitedString(acceptHeader);
- result = MediaType.parseMediaTypes(value);
- }
- }
-
- return result;
+ return MediaType.parseMediaTypes(get(ACCEPT));
}
/**
@@ -442,10 +440,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Returns the value of the {@code Access-Control-Allow-Credentials} response header.
+ * Return the value of the {@code Access-Control-Allow-Credentials} response header.
*/
public boolean getAccessControlAllowCredentials() {
- return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS));
+ return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
/**
@@ -456,10 +454,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Returns the value of the {@code Access-Control-Allow-Headers} response header.
+ * Return the value of the {@code Access-Control-Allow-Headers} response header.
*/
public List<String> getAccessControlAllowHeaders() {
- return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS);
+ return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS);
}
/**
@@ -476,7 +474,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
List<HttpMethod> result = new ArrayList<HttpMethod>();
String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS);
if (value != null) {
- String[] tokens = value.split(",\\s*");
+ String[] tokens = StringUtils.tokenizeToStringArray(value, ",", true, true);
for (String token : tokens) {
HttpMethod resolved = HttpMethod.resolve(token);
if (resolved != null) {
@@ -498,7 +496,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* Return the value of the {@code Access-Control-Allow-Origin} response header.
*/
public String getAccessControlAllowOrigin() {
- return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN);
+ return getFieldValues(ACCESS_CONTROL_ALLOW_ORIGIN);
}
/**
@@ -509,10 +507,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Returns the value of the {@code Access-Control-Expose-Headers} response header.
+ * Return the value of the {@code Access-Control-Expose-Headers} response header.
*/
public List<String> getAccessControlExposeHeaders() {
- return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS);
+ return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS);
}
/**
@@ -523,7 +521,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Returns the value of the {@code Access-Control-Max-Age} response header.
+ * Return the value of the {@code Access-Control-Max-Age} response header.
* <p>Returns -1 when the max age is unknown.
*/
public long getAccessControlMaxAge() {
@@ -539,10 +537,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Returns the value of the {@code Access-Control-Request-Headers} request header.
+ * Return the value of the {@code Access-Control-Request-Headers} request header.
*/
public List<String> getAccessControlRequestHeaders() {
- return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS);
+ return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS);
}
/**
@@ -643,7 +641,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* Return the value of the {@code Cache-Control} header.
*/
public String getCacheControl() {
- return getFirst(CACHE_CONTROL);
+ return getFieldValues(CACHE_CONTROL);
}
/**
@@ -664,7 +662,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* Return the value of the {@code Connection} header.
*/
public List<String> getConnection() {
- return getFirstValueAsList(CONNECTION);
+ return getValuesAsList(CONNECTION);
}
/**
@@ -783,6 +781,30 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
+ * Set the (new) value of the {@code If-Match} header.
+ * @since 4.3
+ */
+ public void setIfMatch(String ifMatch) {
+ set(IF_MATCH, ifMatch);
+ }
+
+ /**
+ * Set the (new) value of the {@code If-Match} header.
+ * @since 4.3
+ */
+ public void setIfMatch(List<String> ifMatchList) {
+ set(IF_MATCH, toCommaDelimitedString(ifMatchList));
+ }
+
+ /**
+ * Return the value of the {@code If-Match} header.
+ * @since 4.3
+ */
+ public List<String> getIfMatch() {
+ return getETagValuesAsList(IF_MATCH);
+ }
+
+ /**
* Set the (new) value of the {@code If-Modified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
@@ -814,35 +836,31 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList));
}
- protected String toCommaDelimitedString(List<String> list) {
- StringBuilder builder = new StringBuilder();
- for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
- String ifNoneMatch = iterator.next();
- builder.append(ifNoneMatch);
- if (iterator.hasNext()) {
- builder.append(", ");
- }
- }
- return builder.toString();
- }
-
/**
* Return the value of the {@code If-None-Match} header.
*/
public List<String> getIfNoneMatch() {
- return getFirstValueAsList(IF_NONE_MATCH);
+ return getETagValuesAsList(IF_NONE_MATCH);
}
- protected List<String> getFirstValueAsList(String header) {
- List<String> result = new ArrayList<String>();
- String value = getFirst(header);
- if (value != null) {
- String[] tokens = value.split(",\\s*");
- for (String token : tokens) {
- result.add(token);
- }
- }
- return result;
+ /**
+ * Set the (new) value of the {@code If-Unmodified-Since} header.
+ * <p>The date should be specified as the number of milliseconds since
+ * January 1, 1970 GMT.
+ * @since 4.3
+ */
+ public void setIfUnmodifiedSince(long ifUnmodifiedSince) {
+ setDate(IF_UNMODIFIED_SINCE, ifUnmodifiedSince);
+ }
+
+ /**
+ * Return the value of the {@code If-Unmodified-Since} header.
+ * <p>The date is returned as the number of milliseconds since
+ * January 1, 1970 GMT. Returns -1 when the date is unknown.
+ * @since 4.3
+ */
+ public long getIfUnmodifiedSince() {
+ return getFirstDate(IF_UNMODIFIED_SINCE, false);
}
/**
@@ -943,11 +961,43 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
+ * Set the request header names (e.g. "Accept-Language") for which the
+ * response is subject to content negotiation and variances based on the
+ * value of those request headers.
+ * @param requestHeaders the request header names
+ * @since 4.3
+ */
+ public void setVary(List<String> requestHeaders) {
+ set(VARY, toCommaDelimitedString(requestHeaders));
+ }
+
+ /**
+ * Return the request header names subject to content negotiation.
+ * @since 4.3
+ */
+ public List<String> getVary() {
+ return getValuesAsList(VARY);
+ }
+
+ /**
+ * Set the given date under the given header name after formatting it as a string
+ * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of
+ * {@link #set(String, String)} but for date headers.
+ * @since 3.2.4
+ */
+ public void setDate(String headerName, long date) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US);
+ dateFormat.setTimeZone(GMT);
+ set(headerName, dateFormat.format(new Date(date)));
+ }
+
+ /**
* Parse the first header value for the given header name as a date,
* return -1 if there is no value, or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @return the parsed date header, or -1 if none
+ * @since 3.2.4
*/
public long getFirstDate(String headerName) {
return getFirstDate(headerName, true);
@@ -992,16 +1042,92 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Set the given date under the given header name after formatting it as a string
- * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of
- * {@link #set(String, String)} but for date headers.
+ * Return all values of a given header name,
+ * even if this header is set multiple times.
+ * @param headerName the header name
+ * @return all associated values
+ * @since 4.3
+ */
+ public List<String> getValuesAsList(String headerName) {
+ List<String> values = get(headerName);
+ if (values != null) {
+ List<String> result = new ArrayList<String>();
+ for (String value : values) {
+ if (value != null) {
+ String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
+ for (String token : tokens) {
+ result.add(token);
+ }
+ }
+ }
+ return result;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Retrieve a combined result from the field values of the ETag header.
+ * @param headerName the header name
+ * @return the combined result
+ * @since 4.3
+ */
+ protected List<String> getETagValuesAsList(String headerName) {
+ List<String> values = get(headerName);
+ if (values != null) {
+ List<String> result = new ArrayList<String>();
+ for (String value : values) {
+ if (value != null) {
+ Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value);
+ while (matcher.find()) {
+ if ("*".equals(matcher.group())) {
+ result.add(matcher.group());
+ }
+ else {
+ result.add(matcher.group(1));
+ }
+ }
+ if (result.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Could not parse header '" + headerName + "' with value '" + value + "'");
+ }
+ }
+ }
+ return result;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Retrieve a combined result from the field values of multi-valued headers.
+ * @param headerName the header name
+ * @return the combined result
+ * @since 4.3
*/
- public void setDate(String headerName, long date) {
- SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US);
- dateFormat.setTimeZone(GMT);
- set(headerName, dateFormat.format(new Date(date)));
+ protected String getFieldValues(String headerName) {
+ List<String> headerValues = get(headerName);
+ return (headerValues != null ? toCommaDelimitedString(headerValues) : null);
+ }
+
+ /**
+ * Turn the given list of header values into a comma-delimited result.
+ * @param headerValues the list of header values
+ * @return a combined result with comma delimitation
+ */
+ protected String toCommaDelimitedString(List<String> headerValues) {
+ StringBuilder builder = new StringBuilder();
+ for (Iterator<String> it = headerValues.iterator(); it.hasNext(); ) {
+ String val = it.next();
+ builder.append(val);
+ if (it.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ return builder.toString();
}
+
+ // MultiValueMap implementation
+
/**
* Return the first header value for the given header name, if any.
* @param headerName the header name
diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java
index 87173fd3..c38c46fc 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java
@@ -61,7 +61,7 @@ public enum HttpMethod {
* @since 4.2.4
*/
public boolean matches(String method) {
- return name().equals(method);
+ return (this == resolve(method));
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpRange.java b/spring-web/src/main/java/org/springframework/http/HttpRange.java
index 29f2e675..63d40e69 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpRange.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,12 +16,16 @@
package org.springframework.http;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.ResourceRegion;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -42,6 +46,30 @@ public abstract class HttpRange {
/**
+ * Turn a {@code Resource} into a {@link ResourceRegion} using the range
+ * information contained in the current {@code HttpRange}.
+ * @param resource the {@code Resource} to select the region from
+ * @return the selected region of the given {@code Resource}
+ * @since 4.3
+ */
+ public ResourceRegion toResourceRegion(Resource resource) {
+ // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
+ // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
+ Assert.isTrue(InputStreamResource.class != resource.getClass(),
+ "Can't convert an InputStreamResource to a ResourceRegion");
+ try {
+ long contentLength = resource.contentLength();
+ Assert.isTrue(contentLength > 0, "Resource content length should be > 0");
+ long start = getRangeStart(contentLength);
+ long end = getRangeEnd(contentLength);
+ return new ResourceRegion(resource, start, end - start + 1);
+ }
+ catch (IOException ex) {
+ throw new IllegalArgumentException("Failed to convert Resource to ResourceRegion", ex);
+ }
+ }
+
+ /**
* Return the start of the range given the total length of a representation.
* @param length the length of the representation
* @return the start of this range for the representation
@@ -134,6 +162,25 @@ public abstract class HttpRange {
}
/**
+ * Convert each {@code HttpRange} into a {@code ResourceRegion},
+ * selecting the appropriate segment of the given {@code Resource}
+ * using the HTTP Range information.
+ * @param ranges the list of ranges
+ * @param resource the resource to select the regions from
+ * @return the list of regions for the given resource
+ */
+ public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) {
+ if(ranges == null || ranges.size() == 0) {
+ return Collections.emptyList();
+ }
+ List<ResourceRegion> regions = new ArrayList<ResourceRegion>(ranges.size());
+ for(HttpRange range : ranges) {
+ regions.add(range.toResourceRegion(resource));
+ }
+ return regions;
+ }
+
+ /**
* Return a string representation of the given list of {@code HttpRange} objects.
* <p>This method can be used to for an {@code Range} header.
* @param ranges the ranges to create a string of
diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java
index d0f9b7c4..2c864624 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -17,12 +17,14 @@
package org.springframework.http;
/**
- * Java 5 enumeration of HTTP status codes.
+ * Enumeration of HTTP status codes.
*
* <p>The HTTP status code series can be retrieved via {@link #series()}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
+ * @author Brian Clozel
+ * @since 3.0
* @see HttpStatus.Series
* @see <a href="http://www.iana.org/assignments/http-status-codes">HTTP Status Code Registry</a>
* @see <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes">List of HTTP status codes - Wikipedia</a>
@@ -321,6 +323,13 @@ public enum HttpStatus {
* @see <a href="http://tools.ietf.org/html/rfc6585#section-5">Additional HTTP Status Codes</a>
*/
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
+ /**
+ * {@code 451 Unavailable For Legal Reasons}.
+ * @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-legally-restricted-status-04">
+ * An HTTP Status Code to Report Legal Obstacles</a>
+ * @since 4.3
+ */
+ UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"),
// --- 5xx Server Error ---
@@ -385,17 +394,17 @@ public enum HttpStatus {
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required");
-
private final int value;
private final String reasonPhrase;
- private HttpStatus(int value, String reasonPhrase) {
+ HttpStatus(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
+
/**
* Return the integer value of this status code.
*/
@@ -407,7 +416,7 @@ public enum HttpStatus {
* Return the reason phrase of this status code.
*/
public String getReasonPhrase() {
- return reasonPhrase;
+ return this.reasonPhrase;
}
/**
@@ -416,7 +425,7 @@ public enum HttpStatus {
* This is a shortcut for checking the value of {@link #series()}.
*/
public boolean is1xxInformational() {
- return (Series.INFORMATIONAL.equals(series()));
+ return Series.INFORMATIONAL.equals(series());
}
/**
@@ -425,7 +434,7 @@ public enum HttpStatus {
* This is a shortcut for checking the value of {@link #series()}.
*/
public boolean is2xxSuccessful() {
- return (Series.SUCCESSFUL.equals(series()));
+ return Series.SUCCESSFUL.equals(series());
}
/**
@@ -434,7 +443,7 @@ public enum HttpStatus {
* This is a shortcut for checking the value of {@link #series()}.
*/
public boolean is3xxRedirection() {
- return (Series.REDIRECTION.equals(series()));
+ return Series.REDIRECTION.equals(series());
}
@@ -444,7 +453,7 @@ public enum HttpStatus {
* This is a shortcut for checking the value of {@link #series()}.
*/
public boolean is4xxClientError() {
- return (Series.CLIENT_ERROR.equals(series()));
+ return Series.CLIENT_ERROR.equals(series());
}
/**
@@ -453,7 +462,7 @@ public enum HttpStatus {
* This is a shortcut for checking the value of {@link #series()}.
*/
public boolean is5xxServerError() {
- return (Series.SERVER_ERROR.equals(series()));
+ return Series.SERVER_ERROR.equals(series());
}
/**
@@ -469,7 +478,7 @@ public enum HttpStatus {
*/
@Override
public String toString() {
- return Integer.toString(value);
+ return Integer.toString(this.value);
}
@@ -490,10 +499,10 @@ public enum HttpStatus {
/**
- * Java 5 enumeration of HTTP status series.
+ * Enumeration of HTTP status series.
* <p>Retrievable via {@link HttpStatus#series()}.
*/
- public static enum Series {
+ public enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
@@ -503,7 +512,7 @@ public enum HttpStatus {
private final int value;
- private Series(int value) {
+ Series(int value) {
this.value = value;
}
@@ -527,7 +536,6 @@ public enum HttpStatus {
public static Series valueOf(HttpStatus status) {
return valueOf(status.value);
}
-
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java
index 8b8e0485..924c1e4c 100644
--- a/spring-web/src/main/java/org/springframework/http/MediaType.java
+++ b/spring-web/src/main/java/org/springframework/http/MediaType.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.InvalidMimeTypeException;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
@@ -42,8 +43,7 @@ import org.springframework.util.comparator.CompoundComparator;
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 3.0
- * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics
- * and Content, section 3.1.1.1</a>
+ * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics and Content, section 3.1.1.1</a>
*/
public class MediaType extends MimeType implements Serializable {
@@ -71,7 +71,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Public constant media type for {@code application/x-www-form-urlencoded}.
- * */
+ */
public final static MediaType APPLICATION_FORM_URLENCODED;
/**
@@ -103,7 +103,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Public constant media type for {@code application/octet-stream}.
- * */
+ */
public final static MediaType APPLICATION_OCTET_STREAM;
/**
@@ -112,8 +112,18 @@ public class MediaType extends MimeType implements Serializable {
public final static String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream";
/**
+ * Public constant media type for {@code application/pdf}.
+ */
+ public final static MediaType APPLICATION_PDF;
+
+ /**
+ * A String equivalent of {@link MediaType#APPLICATION_PDF}.
+ */
+ public final static String APPLICATION_PDF_VALUE = "application/pdf";
+
+ /**
* Public constant media type for {@code application/xhtml+xml}.
- * */
+ */
public final static MediaType APPLICATION_XHTML_XML;
/**
@@ -163,7 +173,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Public constant media type for {@code multipart/form-data}.
- * */
+ */
public final static MediaType MULTIPART_FORM_DATA;
/**
@@ -173,7 +183,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Public constant media type for {@code text/html}.
- * */
+ */
public final static MediaType TEXT_HTML;
/**
@@ -182,8 +192,18 @@ public class MediaType extends MimeType implements Serializable {
public final static String TEXT_HTML_VALUE = "text/html";
/**
+ * Public constant media type for {@code text/markdown}.
+ */
+ public final static MediaType TEXT_MARKDOWN;
+
+ /**
+ * A String equivalent of {@link MediaType#TEXT_MARKDOWN}.
+ */
+ public final static String TEXT_MARKDOWN_VALUE = "text/markdown";
+
+ /**
* Public constant media type for {@code text/plain}.
- * */
+ */
public final static MediaType TEXT_PLAIN;
/**
@@ -193,7 +213,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Public constant media type for {@code text/xml}.
- * */
+ */
public final static MediaType TEXT_XML;
/**
@@ -212,6 +232,7 @@ public class MediaType extends MimeType implements Serializable {
APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE);
APPLICATION_JSON_UTF8 = valueOf(APPLICATION_JSON_UTF8_VALUE);
APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE);
+ APPLICATION_PDF = valueOf(APPLICATION_PDF_VALUE);
APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE);
APPLICATION_XML = valueOf(APPLICATION_XML_VALUE);
IMAGE_GIF = valueOf(IMAGE_GIF_VALUE);
@@ -219,6 +240,7 @@ public class MediaType extends MimeType implements Serializable {
IMAGE_PNG = valueOf(IMAGE_PNG_VALUE);
MULTIPART_FORM_DATA = valueOf(MULTIPART_FORM_DATA_VALUE);
TEXT_HTML = valueOf(TEXT_HTML_VALUE);
+ TEXT_MARKDOWN = valueOf(TEXT_MARKDOWN_VALUE);
TEXT_PLAIN = valueOf(TEXT_PLAIN_VALUE);
TEXT_XML = valueOf(TEXT_XML_VALUE);
}
@@ -268,6 +290,17 @@ public class MediaType extends MimeType implements Serializable {
}
/**
+ * Copy-constructor that copies the type, subtype and parameters of the given
+ * {@code MediaType}, and allows to set the specified character set.
+ * @param other the other media type
+ * @param charset the character set
+ * @throws IllegalArgumentException if any of the parameters contain illegal characters
+ */
+ public MediaType(MediaType other, Charset charset) {
+ super(other, charset);
+ }
+
+ /**
* Copy-constructor that copies the type and subtype of the given {@code MediaType},
* and allows for different parameter.
* @param other the other media type
@@ -364,6 +397,8 @@ public class MediaType extends MimeType implements Serializable {
* Parse the given String value into a {@code MediaType} object,
* with this method name following the 'valueOf' naming convention
* (as supported by {@link org.springframework.core.convert.ConversionService}.
+ * @param value the string to parse
+ * @throws InvalidMediaTypeException if the media type value cannot be parsed
* @see #parseMediaType(String)
*/
public static MediaType valueOf(String value) {
@@ -374,7 +409,7 @@ public class MediaType extends MimeType implements Serializable {
* Parse the given String into a single {@code MediaType}.
* @param mediaType the string to parse
* @return the media type
- * @throws InvalidMediaTypeException if the string cannot be parsed
+ * @throws InvalidMediaTypeException if the media type value cannot be parsed
*/
public static MediaType parseMediaType(String mediaType) {
MimeType type;
@@ -392,13 +427,12 @@ public class MediaType extends MimeType implements Serializable {
}
}
-
/**
- * Parse the given, comma-separated string into a list of {@code MediaType} objects.
+ * Parse the given comma-separated string into a list of {@code MediaType} objects.
* <p>This method can be used to parse an Accept or Content-Type header.
* @param mediaTypes the string to parse
* @return the list of media types
- * @throws IllegalArgumentException if the string cannot be parsed
+ * @throws InvalidMediaTypeException if the media type value cannot be parsed
*/
public static List<MediaType> parseMediaTypes(String mediaTypes) {
if (!StringUtils.hasLength(mediaTypes)) {
@@ -413,6 +447,31 @@ public class MediaType extends MimeType implements Serializable {
}
/**
+ * Parse the given list of (potentially) comma-separated strings into a
+ * list of {@code MediaType} objects.
+ * <p>This method can be used to parse an Accept or Content-Type header.
+ * @param mediaTypes the string to parse
+ * @return the list of media types
+ * @throws InvalidMediaTypeException if the media type value cannot be parsed
+ * @since 4.3.2
+ */
+ public static List<MediaType> parseMediaTypes(List<String> mediaTypes) {
+ if (CollectionUtils.isEmpty(mediaTypes)) {
+ return Collections.<MediaType>emptyList();
+ }
+ else if (mediaTypes.size() == 1) {
+ return parseMediaTypes(mediaTypes.get(0));
+ }
+ else {
+ List<MediaType> result = new ArrayList<MediaType>(8);
+ for (String mediaType : mediaTypes) {
+ result.addAll(parseMediaTypes(mediaType));
+ }
+ return result;
+ }
+ }
+
+ /**
* Return a string representation of the given list of {@code MediaType} objects.
* <p>This method can be used to for an {@code Accept} or {@code Content-Type} header.
* @param mediaTypes the media types to create a string representation for
diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java
index 7e9ab88b..46dbba93 100644
--- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,6 +16,7 @@
package org.springframework.http;
+import java.lang.reflect.Type;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Arrays;
@@ -54,6 +55,7 @@ import org.springframework.util.ObjectUtils;
* </pre>
*
* @author Arjen Poutsma
+ * @author Sebastien Deleuze
* @since 4.1
* @see #getMethod()
* @see #getUrl()
@@ -64,6 +66,8 @@ public class RequestEntity<T> extends HttpEntity<T> {
private final URI url;
+ private final Type type;
+
/**
* Constructor with method and URL but without body nor headers.
@@ -81,7 +85,19 @@ public class RequestEntity<T> extends HttpEntity<T> {
* @param url the URL
*/
public RequestEntity(T body, HttpMethod method, URI url) {
- this(body, null, method, url);
+ this(body, null, method, url, null);
+ }
+
+ /**
+ * Constructor with method, URL, body and type but without headers.
+ * @param body the body
+ * @param method the method
+ * @param url the URL
+ * @param type the type used for generic type resolution
+ * @since 4.3
+ */
+ public RequestEntity(T body, HttpMethod method, URI url, Type type) {
+ this(body, null, method, url, type);
}
/**
@@ -91,7 +107,7 @@ public class RequestEntity<T> extends HttpEntity<T> {
* @param url the URL
*/
public RequestEntity(MultiValueMap<String, String> headers, HttpMethod method, URI url) {
- this(null, headers, method, url);
+ this(null, headers, method, url, null);
}
/**
@@ -102,9 +118,23 @@ public class RequestEntity<T> extends HttpEntity<T> {
* @param url the URL
*/
public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url) {
+ this(body, headers, method, url, null);
+ }
+
+ /**
+ * Constructor with method, URL, headers, body and type.
+ * @param body the body
+ * @param headers the headers
+ * @param method the method
+ * @param url the URL
+ * @param type the type used for generic type resolution
+ * @since 4.3
+ */
+ public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url, Type type) {
super(body, headers);
this.method = method;
this.url = url;
+ this.type = type;
}
@@ -124,6 +154,21 @@ public class RequestEntity<T> extends HttpEntity<T> {
return this.url;
}
+ /**
+ * Return the type of the request's body.
+ * @return the request's body type, or {@code null} if not known
+ * @since 4.3
+ */
+ public Type getType() {
+ if (this.type == null) {
+ T body = getBody();
+ if (body != null) {
+ return body.getClass();
+ }
+ }
+ return this.type;
+ }
+
@Override
public boolean equals(Object other) {
@@ -134,8 +179,8 @@ public class RequestEntity<T> extends HttpEntity<T> {
return false;
}
RequestEntity<?> otherEntity = (RequestEntity<?>) other;
- return (ObjectUtils.nullSafeEquals(this.method, otherEntity.method) &&
- ObjectUtils.nullSafeEquals(this.url, otherEntity.url));
+ return (ObjectUtils.nullSafeEquals(getMethod(), otherEntity.getMethod()) &&
+ ObjectUtils.nullSafeEquals(getUrl(), otherEntity.getUrl()));
}
@Override
@@ -149,9 +194,9 @@ public class RequestEntity<T> extends HttpEntity<T> {
@Override
public String toString() {
StringBuilder builder = new StringBuilder("<");
- builder.append(this.method);
+ builder.append(getMethod());
builder.append(' ');
- builder.append(this.url);
+ builder.append(getUrl());
builder.append(',');
T body = getBody();
HttpHeaders headers = getHeaders();
@@ -327,6 +372,16 @@ public class RequestEntity<T> extends HttpEntity<T> {
* @return the built request entity
*/
<T> RequestEntity<T> body(T body);
+
+ /**
+ * Set the body and type of the request entity and build the RequestEntity.
+ * @param <T> the type of the body
+ * @param body the body of the request entity
+ * @param type the type of the body, useful for generic type resolution
+ * @return the built request entity
+ * @since 4.3
+ */
+ <T> RequestEntity<T> body(T body, Type type);
}
@@ -396,6 +451,11 @@ public class RequestEntity<T> extends HttpEntity<T> {
public <T> RequestEntity<T> body(T body) {
return new RequestEntity<T>(body, this.headers, this.method, this.url);
}
+
+ @Override
+ public <T> RequestEntity<T> body(T body, Type type) {
+ return new RequestEntity<T>(body, this.headers, this.method, this.url, type);
+ }
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java
index 9eba72c0..7edb9ea2 100644
--- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
+import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
@@ -65,45 +66,55 @@ import org.springframework.util.ObjectUtils;
*/
public class ResponseEntity<T> extends HttpEntity<T> {
- private final HttpStatus statusCode;
+ private final Object statusCode;
/**
* Create a new {@code ResponseEntity} with the given status code, and no body nor headers.
- * @param statusCode the status code
+ * @param status the status code
*/
- public ResponseEntity(HttpStatus statusCode) {
- super();
- this.statusCode = statusCode;
+ public ResponseEntity(HttpStatus status) {
+ this(null, null, status);
}
/**
* Create a new {@code ResponseEntity} with the given body and status code, and no headers.
* @param body the entity body
- * @param statusCode the status code
+ * @param status the status code
*/
- public ResponseEntity(T body, HttpStatus statusCode) {
- super(body);
- this.statusCode = statusCode;
+ public ResponseEntity(T body, HttpStatus status) {
+ this(body, null, status);
}
/**
* Create a new {@code HttpEntity} with the given headers and status code, and no body.
* @param headers the entity headers
- * @param statusCode the status code
+ * @param status the status code
*/
- public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus statusCode) {
- super(headers);
- this.statusCode = statusCode;
+ public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status) {
+ this(null, headers, status);
}
/**
* Create a new {@code HttpEntity} with the given body, headers, and status code.
* @param body the entity body
* @param headers the entity headers
- * @param statusCode the status code
+ * @param status the status code
*/
- public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus statusCode) {
+ public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status) {
+ super(body, headers);
+ Assert.notNull(status, "HttpStatus must not be null");
+ this.statusCode = status;
+ }
+
+ /**
+ * Create a new {@code HttpEntity} with the given body, headers, and status code.
+ * Just used behind the nested builder API.
+ * @param body the entity body
+ * @param headers the entity headers
+ * @param statusCode the status code (as {@code HttpStatus} or as {@code Integer} value)
+ */
+ private ResponseEntity(T body, MultiValueMap<String, String> headers, Object statusCode) {
super(body, headers);
this.statusCode = statusCode;
}
@@ -111,10 +122,29 @@ public class ResponseEntity<T> extends HttpEntity<T> {
/**
* Return the HTTP status code of the response.
- * @return the HTTP status as an HttpStatus enum value
+ * @return the HTTP status as an HttpStatus enum entry
*/
public HttpStatus getStatusCode() {
- return this.statusCode;
+ if (this.statusCode instanceof HttpStatus) {
+ return (HttpStatus) this.statusCode;
+ }
+ else {
+ return HttpStatus.valueOf((Integer) this.statusCode);
+ }
+ }
+
+ /**
+ * Return the HTTP status code of the response.
+ * @return the HTTP status as an int value
+ * @since 4.3
+ */
+ public int getStatusCodeValue() {
+ if (this.statusCode instanceof HttpStatus) {
+ return ((HttpStatus) this.statusCode).value();
+ }
+ else {
+ return (Integer) this.statusCode;
+ }
}
@@ -139,8 +169,10 @@ public class ResponseEntity<T> extends HttpEntity<T> {
public String toString() {
StringBuilder builder = new StringBuilder("<");
builder.append(this.statusCode.toString());
- builder.append(' ');
- builder.append(this.statusCode.getReasonPhrase());
+ if (this.statusCode instanceof HttpStatus) {
+ builder.append(' ');
+ builder.append(((HttpStatus) this.statusCode).getReasonPhrase());
+ }
builder.append(',');
T body = getBody();
HttpHeaders headers = getHeaders();
@@ -167,6 +199,7 @@ public class ResponseEntity<T> extends HttpEntity<T> {
* @since 4.1
*/
public static BodyBuilder status(HttpStatus status) {
+ Assert.notNull(status, "HttpStatus must not be null");
return new DefaultBuilder(status);
}
@@ -177,7 +210,7 @@ public class ResponseEntity<T> extends HttpEntity<T> {
* @since 4.1
*/
public static BodyBuilder status(int status) {
- return status(HttpStatus.valueOf(status));
+ return new DefaultBuilder(status);
}
/**
@@ -333,6 +366,17 @@ public class ResponseEntity<T> extends HttpEntity<T> {
B cacheControl(CacheControl cacheControl);
/**
+ * Configure one or more request header names (e.g. "Accept-Language") to
+ * add to the "Vary" response header to inform clients that the response is
+ * subject to content negotiation and variances based on the value of the
+ * given request headers. The configured request header names are added only
+ * if not already present in the response "Vary" header.
+ * @param requestHeaders request header names
+ * @since 4.3
+ */
+ B varyBy(String... requestHeaders);
+
+ /**
* Build the response entity with no body.
* @return the response entity
* @see BodyBuilder#body(Object)
@@ -377,12 +421,12 @@ public class ResponseEntity<T> extends HttpEntity<T> {
private static class DefaultBuilder implements BodyBuilder {
- private final HttpStatus status;
+ private final Object statusCode;
private final HttpHeaders headers = new HttpHeaders();
- public DefaultBuilder(HttpStatus status) {
- this.status = status;
+ public DefaultBuilder(Object statusCode) {
+ this.statusCode = statusCode;
}
@Override
@@ -455,13 +499,19 @@ public class ResponseEntity<T> extends HttpEntity<T> {
}
@Override
+ public BodyBuilder varyBy(String... requestHeaders) {
+ this.headers.setVary(Arrays.asList(requestHeaders));
+ return this;
+ }
+
+ @Override
public ResponseEntity<Void> build() {
- return new ResponseEntity<Void>(null, this.headers, this.status);
+ return body(null);
}
@Override
public <T> ResponseEntity<T> body(T body) {
- return new ResponseEntity<T>(body, this.headers, this.status);
+ return new ResponseEntity<T>(body, this.headers, this.statusCode);
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java
new file mode 100644
index 00000000..2f09014a
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+
+import org.springframework.http.HttpRequest;
+import org.springframework.util.concurrent.ListenableFuture;
+
+/**
+ * Represents the context of a client-side HTTP request execution.
+ *
+ * <p>Used to invoke the next interceptor in the interceptor chain, or -
+ * if the calling interceptor is last - execute the request itself.
+ *
+ * @author Jakub Narloch
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ * @see AsyncClientHttpRequestInterceptor
+ */
+public interface AsyncClientHttpRequestExecution {
+
+ /**
+ * Resume the request execution by invoking the next interceptor in the chain
+ * or executing the request to the remote service.
+ * @param request the HTTP request, containing the HTTP method and headers
+ * @param body the body of the request
+ * @return a corresponding future handle
+ * @throws IOException in case of I/O errors
+ */
+ ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body) throws IOException;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java
new file mode 100644
index 00000000..3fabe120
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.support.InterceptingAsyncHttpAccessor;
+import org.springframework.util.concurrent.ListenableFuture;
+
+/**
+ * Intercepts client-side HTTP requests. Implementations of this interface can be
+ * {@linkplain org.springframework.web.client.AsyncRestTemplate#setInterceptors registered}
+ * with the {@link org.springframework.web.client.AsyncRestTemplate} as to modify
+ * the outgoing {@link HttpRequest} and/or register to modify the incoming
+ * {@link ClientHttpResponse} with help of a
+ * {@link org.springframework.util.concurrent.ListenableFutureAdapter}.
+ *
+ * <p>The main entry point for interceptors is {@link #intercept}.
+ *
+ * @author Jakub Narloch
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ * @see org.springframework.web.client.AsyncRestTemplate
+ * @see InterceptingAsyncHttpAccessor
+ */
+public interface AsyncClientHttpRequestInterceptor {
+
+ /**
+ * Intercept the given request, and return a response future. The given
+ * {@link AsyncClientHttpRequestExecution} allows the interceptor to pass on
+ * the request to the next entity in the chain.
+ * <p>An implementation might follow this pattern:
+ * <ol>
+ * <li>Examine the {@linkplain HttpRequest request} and body</li>
+ * <li>Optionally {@linkplain org.springframework.http.client.support.HttpRequestWrapper
+ * wrap} the request to filter HTTP attributes.</li>
+ * <li>Optionally modify the body of the request.</li>
+ * <li>One of the following:
+ * <ul>
+ * <li>execute the request through {@link ClientHttpRequestExecution}</li>
+ * <li>don't execute the request to block the execution altogether</li>
+ * </ul>
+ * <li>Optionally adapt the response to filter HTTP attributes with the help of
+ * {@link org.springframework.util.concurrent.ListenableFutureAdapter
+ * ListenableFutureAdapter}.</li>
+ * </ol>
+ * @param request the request, containing method, URI, and headers
+ * @param body the body of the request
+ * @param execution the request execution
+ * @return the response future
+ * @throws IOException in case of I/O errors
+ */
+ ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body,
+ AsyncClientHttpRequestExecution execution) throws IOException;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java
index df6e10c6..2fe64f52 100644
--- a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java
+++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -23,22 +23,23 @@ import org.springframework.http.HttpRequest;
/**
* Represents the context of a client-side HTTP request execution.
*
- * <p>Used to invoke the next interceptor in the interceptor chain, or - if the calling interceptor is last - execute
- * the request itself.
+ * <p>Used to invoke the next interceptor in the interceptor chain,
+ * or - if the calling interceptor is last - execute the request itself.
*
* @author Arjen Poutsma
- * @see ClientHttpRequestInterceptor
* @since 3.1
+ * @see ClientHttpRequestInterceptor
*/
public interface ClientHttpRequestExecution {
/**
- * Execute the request with the given request attributes and body, and return the response.
- *
+ * Execute the request with the given request attributes and body,
+ * and return the response.
* @param request the request, containing method, URI, and headers
* @param body the body of the request to execute
* @return the response
* @throws IOException in case of I/O errors
*/
ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException;
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java
index 53f6faf0..d61231d9 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -39,6 +39,7 @@ import org.apache.http.protocol.HttpContext;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
/**
* {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that
@@ -58,6 +59,20 @@ import org.springframework.util.Assert;
*/
public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {
+ private static Class<?> abstractHttpClientClass;
+
+ static {
+ try {
+ // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3)
+ abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient",
+ HttpComponentsClientHttpRequestFactory.class.getClassLoader());
+ }
+ catch (ClassNotFoundException ex) {
+ // Probably removed from HttpComponents in the meantime...
+ }
+ }
+
+
private HttpClient httpClient;
private RequestConfig requestConfig;
@@ -130,7 +145,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
*/
@SuppressWarnings("deprecation")
private void setLegacyConnectionTimeout(HttpClient client, int timeout) {
- if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) {
+ if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
}
}
@@ -171,7 +186,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
*/
@SuppressWarnings("deprecation")
private void setLegacySocketTimeout(HttpClient client, int timeout) {
- if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) {
+ if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout);
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java
new file mode 100644
index 00000000..0b13181a
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2002-2015 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.client;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Iterator;
+import java.util.List;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpRequest;
+import org.springframework.util.StreamUtils;
+import org.springframework.util.concurrent.ListenableFuture;
+
+/**
+ * An {@link AsyncClientHttpRequest} wrapper that enriches it proceeds the actual
+ * request execution with calling the registered interceptors.
+ *
+ * @author Jakub Narloch
+ * @author Rossen Stoyanchev
+ * @see InterceptingAsyncClientHttpRequestFactory
+ */
+class InterceptingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest {
+
+ private AsyncClientHttpRequestFactory requestFactory;
+
+ private List<AsyncClientHttpRequestInterceptor> interceptors;
+
+ private URI uri;
+
+ private HttpMethod httpMethod;
+
+
+ /**
+ * Creates new instance of {@link InterceptingAsyncClientHttpRequest}.
+ *
+ * @param requestFactory the async request factory
+ * @param interceptors the list of interceptors
+ * @param uri the request URI
+ * @param httpMethod the HTTP method
+ */
+ public InterceptingAsyncClientHttpRequest(AsyncClientHttpRequestFactory requestFactory,
+ List<AsyncClientHttpRequestInterceptor> interceptors, URI uri, HttpMethod httpMethod) {
+
+ this.requestFactory = requestFactory;
+ this.interceptors = interceptors;
+ this.uri = uri;
+ this.httpMethod = httpMethod;
+ }
+
+
+ @Override
+ protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] body)
+ throws IOException {
+
+ return new AsyncRequestExecution().executeAsync(this, body);
+ }
+
+ @Override
+ public HttpMethod getMethod() {
+ return httpMethod;
+ }
+
+ @Override
+ public URI getURI() {
+ return uri;
+ }
+
+
+ private class AsyncRequestExecution implements AsyncClientHttpRequestExecution {
+
+ private Iterator<AsyncClientHttpRequestInterceptor> iterator;
+
+ public AsyncRequestExecution() {
+ this.iterator = interceptors.iterator();
+ }
+
+ @Override
+ public ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body)
+ throws IOException {
+
+ if (this.iterator.hasNext()) {
+ AsyncClientHttpRequestInterceptor interceptor = this.iterator.next();
+ return interceptor.intercept(request, body, this);
+ }
+ else {
+ URI theUri = request.getURI();
+ HttpMethod theMethod = request.getMethod();
+ HttpHeaders theHeaders = request.getHeaders();
+
+ AsyncClientHttpRequest delegate = requestFactory.createAsyncRequest(theUri, theMethod);
+ delegate.getHeaders().putAll(theHeaders);
+ if (body.length > 0) {
+ StreamUtils.copy(body, delegate.getBody());
+ }
+
+ return delegate.executeAsync();
+ }
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java
new file mode 100644
index 00000000..1ca68c52
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.http.HttpMethod;
+
+/**
+ * Wrapper for a {@link AsyncClientHttpRequestFactory} that has support for
+ * {@link AsyncClientHttpRequestInterceptor}s.
+ *
+ * @author Jakub Narloch
+ * @since 4.3
+ * @see InterceptingAsyncClientHttpRequest
+ */
+public class InterceptingAsyncClientHttpRequestFactory implements AsyncClientHttpRequestFactory {
+
+ private AsyncClientHttpRequestFactory delegate;
+
+ private List<AsyncClientHttpRequestInterceptor> interceptors;
+
+
+ /**
+ * Create new instance of {@link InterceptingAsyncClientHttpRequestFactory}
+ * with delegated request factory and list of interceptors.
+ * @param delegate the request factory to delegate to
+ * @param interceptors the list of interceptors to use
+ */
+ public InterceptingAsyncClientHttpRequestFactory(AsyncClientHttpRequestFactory delegate,
+ List<AsyncClientHttpRequestInterceptor> interceptors) {
+
+ this.delegate = delegate;
+ this.interceptors = (interceptors != null ? interceptors : Collections.<AsyncClientHttpRequestInterceptor>emptyList());
+ }
+
+
+ @Override
+ public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod method) {
+ return new InterceptingAsyncClientHttpRequest(this.delegate, this.interceptors, uri, method);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java
index be619f1f..2baa53a4 100644
--- a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java
@@ -56,11 +56,13 @@ class Netty4ClientHttpResponse extends AbstractClientHttpResponse {
@Override
+ @SuppressWarnings("deprecation")
public int getRawStatusCode() throws IOException {
return this.nettyResponse.getStatus().code();
}
@Override
+ @SuppressWarnings("deprecation")
public String getStatusText() throws IOException {
return this.nettyResponse.getStatus().reasonPhrase();
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java
new file mode 100644
index 00000000..057a1652
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+import java.net.URI;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.SettableListenableFuture;
+
+/**
+ * {@link AsyncClientHttpRequest} implementation that uses OkHttp 3.x to execute requests.
+ *
+ * <p>Created via the {@link OkHttp3ClientHttpRequestFactory}.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @author Roy Clarkson
+ * @since 4.3
+ */
+class OkHttp3AsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest {
+
+ private final OkHttpClient client;
+
+ private final URI uri;
+
+ private final HttpMethod method;
+
+
+ public OkHttp3AsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) {
+ this.client = client;
+ this.uri = uri;
+ this.method = method;
+ }
+
+
+ @Override
+ public HttpMethod getMethod() {
+ return this.method;
+ }
+
+ @Override
+ public URI getURI() {
+ return this.uri;
+ }
+
+ @Override
+ protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content)
+ throws IOException {
+
+ Request request = OkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method);
+ return new OkHttpListenableFuture(this.client.newCall(request));
+ }
+
+
+ private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> {
+
+ private final Call call;
+
+ public OkHttpListenableFuture(Call call) {
+ this.call = call;
+ this.call.enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ set(new OkHttp3ClientHttpResponse(response));
+ }
+ @Override
+ public void onFailure(Call call, IOException ex) {
+ setException(ex);
+ }
+ });
+ }
+
+ @Override
+ protected void interruptTask() {
+ this.call.cancel();
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java
new file mode 100644
index 00000000..e0ebaac3
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+import java.net.URI;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+
+/**
+ * {@link ClientHttpRequest} implementation that uses OkHttp 3.x to execute requests.
+ *
+ * <p>Created via the {@link OkHttp3ClientHttpRequestFactory}.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @author Roy Clarkson
+ * @since 4.3
+ */
+class OkHttp3ClientHttpRequest extends AbstractBufferingClientHttpRequest {
+
+ private final OkHttpClient client;
+
+ private final URI uri;
+
+ private final HttpMethod method;
+
+
+ public OkHttp3ClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) {
+ this.client = client;
+ this.uri = uri;
+ this.method = method;
+ }
+
+
+ @Override
+ public HttpMethod getMethod() {
+ return this.method;
+ }
+
+ @Override
+ public URI getURI() {
+ return this.uri;
+ }
+
+
+ @Override
+ protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException {
+ Request request = OkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method);
+ return new OkHttp3ClientHttpResponse(this.client.newCall(request).execute());
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java
new file mode 100644
index 00000000..546fde34
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link ClientHttpRequestFactory} implementation that uses
+ * <a href="http://square.github.io/okhttp/">OkHttp</a> 3.x to create requests.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @author Roy Clarkson
+ * @since 4.3
+ */
+public class OkHttp3ClientHttpRequestFactory
+ implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean {
+
+ private OkHttpClient client;
+
+ private final boolean defaultClient;
+
+
+ /**
+ * Create a factory with a default {@link OkHttpClient} instance.
+ */
+ public OkHttp3ClientHttpRequestFactory() {
+ this.client = new OkHttpClient();
+ this.defaultClient = true;
+ }
+
+ /**
+ * Create a factory with the given {@link OkHttpClient} instance.
+ * @param client the client to use
+ */
+ public OkHttp3ClientHttpRequestFactory(OkHttpClient client) {
+ Assert.notNull(client, "OkHttpClient must not be null");
+ this.client = client;
+ this.defaultClient = false;
+ }
+
+
+ /**
+ * Sets the underlying read timeout in milliseconds.
+ * A value of 0 specifies an infinite timeout.
+ * @see okhttp3.OkHttpClient.Builder#readTimeout(long, TimeUnit)
+ */
+ public void setReadTimeout(int readTimeout) {
+ this.client = this.client.newBuilder()
+ .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
+ .build();
+ }
+
+ /**
+ * Sets the underlying write timeout in milliseconds.
+ * A value of 0 specifies an infinite timeout.
+ * @see okhttp3.OkHttpClient.Builder#writeTimeout(long, TimeUnit)
+ */
+ public void setWriteTimeout(int writeTimeout) {
+ this.client = this.client.newBuilder()
+ .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
+ .build();
+ }
+
+ /**
+ * Sets the underlying connect timeout in milliseconds.
+ * A value of 0 specifies an infinite timeout.
+ * @see okhttp3.OkHttpClient.Builder#connectTimeout(long, TimeUnit)
+ */
+ public void setConnectTimeout(int connectTimeout) {
+ this.client = this.client.newBuilder()
+ .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
+ .build();
+ }
+
+
+ @Override
+ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
+ return new OkHttp3ClientHttpRequest(this.client, uri, httpMethod);
+ }
+
+ @Override
+ public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) {
+ return new OkHttp3AsyncClientHttpRequest(this.client, uri, httpMethod);
+ }
+
+
+ @Override
+ public void destroy() throws IOException {
+ if (this.defaultClient) {
+ // Clean up the client if we created it in the constructor
+ if (this.client.cache() != null) {
+ this.client.cache().close();
+ }
+ this.client.dispatcher().executorService().shutdown();
+ }
+ }
+
+
+ static Request buildRequest(HttpHeaders headers, byte[] content, URI uri,
+ HttpMethod method) throws MalformedURLException {
+
+ okhttp3.MediaType contentType = getContentType(headers);
+ RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null);
+
+ URL url = uri.toURL();
+ String methodName = method.name();
+ Request.Builder builder = new Request.Builder().url(url).method(methodName, body);
+
+ for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+ String headerName = entry.getKey();
+ for (String headerValue : entry.getValue()) {
+ builder.addHeader(headerName, headerValue);
+ }
+ }
+
+ return builder.build();
+ }
+
+ private static okhttp3.MediaType getContentType(HttpHeaders headers) {
+ String rawContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE);
+ return (StringUtils.hasText(rawContentType) ? okhttp3.MediaType.parse(rawContentType) : null);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java
new file mode 100644
index 00000000..b6a7ed19
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import okhttp3.Response;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.Assert;
+
+/**
+ * {@link ClientHttpResponse} implementation based on OkHttp 3.x.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @author Roy Clarkson
+ * @since 4.3
+ */
+class OkHttp3ClientHttpResponse extends AbstractClientHttpResponse {
+
+ private final Response response;
+
+ private HttpHeaders headers;
+
+
+ public OkHttp3ClientHttpResponse(Response response) {
+ Assert.notNull(response, "Response must not be null");
+ this.response = response;
+ }
+
+
+ @Override
+ public int getRawStatusCode() {
+ return this.response.code();
+ }
+
+ @Override
+ public String getStatusText() {
+ return this.response.message();
+ }
+
+ @Override
+ public InputStream getBody() throws IOException {
+ return this.response.body().byteStream();
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ if (this.headers == null) {
+ HttpHeaders headers = new HttpHeaders();
+ for (String headerName : this.response.headers().names()) {
+ for (String headerValue : this.response.headers(headerName)) {
+ headers.add(headerName, headerValue);
+ }
+ }
+ this.headers = headers;
+ }
+ return this.headers;
+ }
+
+ @Override
+ public void close() {
+ this.response.body().close();
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java
new file mode 100644
index 00000000..6b042422
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import java.io.IOException;
+import java.net.URI;
+
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.Callback;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.SettableListenableFuture;
+
+/**
+ * {@link AsyncClientHttpRequest} implementation that uses OkHttp 2.x to execute requests.
+ *
+ * <p>Created via the {@link OkHttpClientHttpRequestFactory}.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @since 4.3
+ * @see org.springframework.http.client.OkHttp3AsyncClientHttpRequest
+ */
+class OkHttpAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest {
+
+ private final OkHttpClient client;
+
+ private final URI uri;
+
+ private final HttpMethod method;
+
+
+ public OkHttpAsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) {
+ this.client = client;
+ this.uri = uri;
+ this.method = method;
+ }
+
+
+ @Override
+ public HttpMethod getMethod() {
+ return this.method;
+ }
+
+ @Override
+ public URI getURI() {
+ return this.uri;
+ }
+
+ @Override
+ protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content)
+ throws IOException {
+
+ Request request = OkHttpClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method);
+ return new OkHttpListenableFuture(this.client.newCall(request));
+ }
+
+
+ private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> {
+
+ private final Call call;
+
+ public OkHttpListenableFuture(Call call) {
+ this.call = call;
+ this.call.enqueue(new Callback() {
+ @Override
+ public void onResponse(Response response) {
+ set(new OkHttpClientHttpResponse(response));
+ }
+ @Override
+ public void onFailure(Request request, IOException ex) {
+ setException(ex);
+ }
+ });
+ }
+
+ @Override
+ protected void interruptTask() {
+ this.call.cancel();
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java
index fe519f06..a2f75be4 100644
--- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -18,35 +18,24 @@ package org.springframework.http.client;
import java.io.IOException;
import java.net.URI;
-import java.net.URL;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-
-import com.squareup.okhttp.Call;
-import com.squareup.okhttp.Callback;
-import com.squareup.okhttp.MediaType;
+
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
-import com.squareup.okhttp.RequestBody;
-import com.squareup.okhttp.Response;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
-import org.springframework.util.StringUtils;
-import org.springframework.util.concurrent.ListenableFuture;
-import org.springframework.util.concurrent.SettableListenableFuture;
/**
- * {@link ClientHttpRequest} implementation that uses OkHttp to execute requests.
+ * {@link ClientHttpRequest} implementation that uses OkHttp 2.x to execute requests.
*
* <p>Created via the {@link OkHttpClientHttpRequestFactory}.
*
* @author Luciano Leggieri
* @author Arjen Poutsma
* @since 4.2
+ * @see org.springframework.http.client.OkHttp3ClientHttpRequest
*/
-class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest implements ClientHttpRequest {
+class OkHttpClientHttpRequest extends AbstractBufferingClientHttpRequest {
private final OkHttpClient client;
@@ -72,73 +61,11 @@ class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest im
return this.uri;
}
- @Override
- protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content)
- throws IOException {
-
- MediaType contentType = getContentType(headers);
- RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null);
-
- URL url = this.uri.toURL();
- String methodName = this.method.name();
- Request.Builder builder = new Request.Builder().url(url).method(methodName, body);
-
- for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
- String headerName = entry.getKey();
- for (String headerValue : entry.getValue()) {
- builder.addHeader(headerName, headerValue);
- }
- }
- Request request = builder.build();
-
- return new OkHttpListenableFuture(this.client.newCall(request));
- }
-
- private MediaType getContentType(HttpHeaders headers) {
- String rawContentType = headers.getFirst("Content-Type");
- return (StringUtils.hasText(rawContentType) ? MediaType.parse(rawContentType) : null);
- }
@Override
- public ClientHttpResponse execute() throws IOException {
- try {
- return executeAsync().get();
- }
- catch (InterruptedException ex) {
- throw new IOException(ex.getMessage(), ex);
- }
- catch (ExecutionException ex) {
- Throwable cause = ex.getCause();
- if (cause instanceof IOException) {
- throw (IOException) cause;
- }
- throw new IOException(cause.getMessage(), cause);
- }
- }
-
-
- private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> {
-
- private final Call call;
-
- public OkHttpListenableFuture(Call call) {
- this.call = call;
- this.call.enqueue(new Callback() {
- @Override
- public void onResponse(Response response) {
- set(new OkHttpClientHttpResponse(response));
- }
- @Override
- public void onFailure(Request request, IOException ex) {
- setException(ex);
- }
- });
- }
-
- @Override
- protected void interruptTask() {
- this.call.cancel();
- }
+ protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException {
+ Request request = OkHttpClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method);
+ return new OkHttpClientHttpResponse(this.client.newCall(request).execute());
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java
index 9b2674dd..d5ae9e97 100644
--- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java
@@ -16,22 +16,32 @@
package org.springframework.http.client;
+import java.io.IOException;
+import java.net.MalformedURLException;
import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import org.springframework.beans.factory.DisposableBean;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
/**
* {@link ClientHttpRequestFactory} implementation that uses
- * <a href="http://square.github.io/okhttp/">OkHttp</a> to create requests.
+ * <a href="http://square.github.io/okhttp/">OkHttp</a> 2.x to create requests.
*
* @author Luciano Leggieri
* @author Arjen Poutsma
* @since 4.2
+ * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory
*/
public class OkHttpClientHttpRequestFactory
implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean {
@@ -90,20 +100,17 @@ public class OkHttpClientHttpRequestFactory
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
- return createRequestInternal(uri, httpMethod);
+ return new OkHttpClientHttpRequest(this.client, uri, httpMethod);
}
@Override
public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) {
- return createRequestInternal(uri, httpMethod);
+ return new OkHttpAsyncClientHttpRequest(this.client, uri, httpMethod);
}
- private OkHttpClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) {
- return new OkHttpClientHttpRequest(this.client, uri, httpMethod);
- }
@Override
- public void destroy() throws Exception {
+ public void destroy() throws IOException {
if (this.defaultClient) {
// Clean up the client if we created it in the constructor
if (this.client.getCache() != null) {
@@ -113,4 +120,31 @@ public class OkHttpClientHttpRequestFactory
}
}
+
+ static Request buildRequest(HttpHeaders headers, byte[] content, URI uri,
+ HttpMethod method) throws MalformedURLException {
+
+ com.squareup.okhttp.MediaType contentType = getContentType(headers);
+ RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null);
+
+ URL url = uri.toURL();
+ String methodName = method.name();
+ Request.Builder builder = new Request.Builder().url(url).method(methodName, body);
+
+ for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+ String headerName = entry.getKey();
+ for (String headerValue : entry.getValue()) {
+ builder.addHeader(headerName, headerValue);
+ }
+ }
+
+ return builder.build();
+ }
+
+ private static com.squareup.okhttp.MediaType getContentType(HttpHeaders headers) {
+ String rawContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE);
+ return (StringUtils.hasText(rawContentType) ?
+ com.squareup.okhttp.MediaType.parse(rawContentType) : null);
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java
index 392a2d7d..6a0639f5 100644
--- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java
@@ -25,11 +25,12 @@ import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
/**
- * {@link ClientHttpResponse} implementation based on OkHttp.
+ * {@link ClientHttpResponse} implementation based on OkHttp 2.x.
*
* @author Luciano Leggieri
* @author Arjen Poutsma
* @since 4.2
+ * @see org.springframework.http.client.OkHttp3ClientHttpResponse
*/
class OkHttpClientHttpResponse extends AbstractClientHttpResponse {
diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java
index 015a2e42..bd439963 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -89,6 +89,10 @@ final class SimpleBufferingAsyncClientHttpRequest extends AbstractBufferingAsync
if (connection.getDoOutput()) {
FileCopyUtils.copy(bufferedOutput, connection.getOutputStream());
}
+ else {
+ // Immediately trigger the request in a no-output scenario as well
+ connection.getResponseCode();
+ }
return new SimpleClientHttpResponse(connection);
}
});
diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java
index 24195beb..9a992f33 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -68,12 +68,10 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
addHeaders(this.connection, headers);
-
// JDK <1.8 doesn't support getOutputStream with HTTP DELETE
if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) {
this.connection.setDoOutput(false);
}
-
if (this.connection.getDoOutput() && this.outputStreaming) {
this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
}
@@ -81,7 +79,10 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp
if (this.connection.getDoOutput()) {
FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
}
-
+ else {
+ // Immediately trigger the request in a no-output scenario as well
+ this.connection.getResponseCode();
+ }
return new SimpleClientHttpResponse(this.connection);
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java
index cc7e627d..f667cb2d 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -21,6 +21,7 @@ import java.io.InputStream;
import java.net.HttpURLConnection;
import org.springframework.http.HttpHeaders;
+import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
@@ -29,6 +30,7 @@ import org.springframework.util.StringUtils;
* {@link SimpleStreamingClientHttpRequest#execute()}.
*
* @author Arjen Poutsma
+ * @author Brian Clozel
* @since 3.0
*/
final class SimpleClientHttpResponse extends AbstractClientHttpResponse {
@@ -37,6 +39,8 @@ final class SimpleClientHttpResponse extends AbstractClientHttpResponse {
private HttpHeaders headers;
+ private InputStream responseStream;
+
SimpleClientHttpResponse(HttpURLConnection connection) {
this.connection = connection;
@@ -78,12 +82,19 @@ final class SimpleClientHttpResponse extends AbstractClientHttpResponse {
@Override
public InputStream getBody() throws IOException {
InputStream errorStream = this.connection.getErrorStream();
- return (errorStream != null ? errorStream : this.connection.getInputStream());
+ this.responseStream = (errorStream != null ? errorStream : this.connection.getInputStream());
+ return this.responseStream;
}
@Override
public void close() {
- this.connection.disconnect();
+ if (this.responseStream != null) {
+ try {
+ StreamUtils.drain(this.responseStream);
+ this.responseStream.close();
+ }
+ catch (IOException e) { }
+ }
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java
index 0417eef0..e3327d23 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -108,6 +108,8 @@ final class SimpleStreamingAsyncClientHttpRequest extends AbstractAsyncClientHtt
else {
SimpleBufferingClientHttpRequest.addHeaders(connection, headers);
connection.connect();
+ // Immediately trigger the request in a no-output scenario as well
+ connection.getResponseCode();
}
}
catch (IOException ex) {
diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java
index 5e871d00..98ca2902 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -94,6 +94,8 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest {
else {
SimpleBufferingClientHttpRequest.addHeaders(this.connection, headers);
this.connection.connect();
+ // Immediately trigger the request in a no-output scenario as well
+ this.connection.getResponseCode();
}
}
catch (IOException ex) {
diff --git a/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java
new file mode 100644
index 00000000..ebf4a536
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2016 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.client.support;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.util.Assert;
+import org.springframework.util.Base64Utils;
+
+/**
+ * {@link ClientHttpRequestInterceptor} to apply a BASIC authorization header.
+ *
+ * @author Phillip Webb
+ * @since 4.3.1
+ */
+public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor {
+
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private final String username;
+
+ private final String password;
+
+
+ /**
+ * Create a new interceptor which adds a BASIC authorization header
+ * for the given username and password.
+ * @param username the username to use
+ * @param password the password to use
+ */
+ public BasicAuthorizationInterceptor(String username, String password) {
+ Assert.hasLength(username, "Username must not be empty");
+ this.username = username;
+ this.password = (password != null ? password : "");
+ }
+
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body,
+ ClientHttpRequestExecution execution) throws IOException {
+
+ String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(UTF_8));
+ request.getHeaders().add("Authorization", "Basic " + token);
+ return execution.execute(request, body);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java
new file mode 100644
index 00000000..fd59f604
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2016 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.client.support;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.http.client.AsyncClientHttpRequestFactory;
+import org.springframework.http.client.AsyncClientHttpRequestInterceptor;
+import org.springframework.http.client.InterceptingAsyncClientHttpRequestFactory;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * The HTTP accessor that extends the base {@link AsyncHttpAccessor} with
+ * request intercepting functionality.
+ *
+ * @author Jakub Narloch
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+public abstract class InterceptingAsyncHttpAccessor extends AsyncHttpAccessor {
+
+ private List<AsyncClientHttpRequestInterceptor> interceptors =
+ new ArrayList<AsyncClientHttpRequestInterceptor>();
+
+
+ /**
+ * Set the request interceptors that this accessor should use.
+ * @param interceptors the list of interceptors
+ */
+ public void setInterceptors(List<AsyncClientHttpRequestInterceptor> interceptors) {
+ this.interceptors = interceptors;
+ }
+
+ /**
+ * Return the request interceptor that this accessor uses.
+ */
+ public List<AsyncClientHttpRequestInterceptor> getInterceptors() {
+ return this.interceptors;
+ }
+
+
+ @Override
+ public AsyncClientHttpRequestFactory getAsyncRequestFactory() {
+ AsyncClientHttpRequestFactory delegate = super.getAsyncRequestFactory();
+ if (!CollectionUtils.isEmpty(getInterceptors())) {
+ return new InterceptingAsyncClientHttpRequestFactory(delegate, getInterceptors());
+ }
+ else {
+ return delegate;
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java
index d63d2870..e990fcd7 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java
@@ -18,6 +18,7 @@ package org.springframework.http.converter;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -42,6 +43,7 @@ import org.springframework.util.Assert;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
+ * @author Sebastien Deleuze
* @since 3.0
*/
public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
@@ -51,6 +53,8 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
private List<MediaType> supportedMediaTypes = Collections.emptyList();
+ private Charset defaultCharset;
+
/**
* Construct an {@code AbstractHttpMessageConverter} with no supported media types.
@@ -75,6 +79,18 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}
+ /**
+ * Construct an {@code AbstractHttpMessageConverter} with a default charset and
+ * multiple supported media types.
+ * @param defaultCharset the default character set
+ * @param supportedMediaTypes the supported media types
+ * @since 4.3
+ */
+ protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) {
+ this.defaultCharset = defaultCharset;
+ setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
+ }
+
/**
* Set the list of {@link MediaType} objects supported by this converter.
@@ -89,6 +105,22 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
return Collections.unmodifiableList(this.supportedMediaTypes);
}
+ /**
+ * Set the default character set, if any.
+ * @since 4.3
+ */
+ public void setDefaultCharset(Charset defaultCharset) {
+ this.defaultCharset = defaultCharset;
+ }
+
+ /**
+ * Return the default character set, if any.
+ * @since 4.3
+ */
+ public Charset getDefaultCharset() {
+ return this.defaultCharset;
+ }
+
/**
* This implementation checks if the given class is {@linkplain #supports(Class) supported},
@@ -200,7 +232,8 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
/**
* Add default headers to the output message.
* <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content
- * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers.
+ * type was not provided, set if necessary the default character set, calls
+ * {@link #getContentLength}, and sets the corresponding headers.
* @since 4.2
*/
protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
@@ -214,6 +247,12 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
+ if (contentTypeToUse.getCharset() == null) {
+ Charset defaultCharset = getDefaultCharset();
+ if (defaultCharset != null) {
+ contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
+ }
+ }
headers.setContentType(contentTypeToUse);
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
index fa2d0e4a..643a8713 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java
@@ -82,6 +82,7 @@ import org.springframework.util.StringUtils;
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.0
* @see MultiValueMap
*/
@@ -90,14 +91,14 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
- private Charset charset = DEFAULT_CHARSET;
-
- private Charset multipartCharset;
-
private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
+ private Charset charset = DEFAULT_CHARSET;
+
+ private Charset multipartCharset;
+
public FormHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
@@ -108,30 +109,10 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
stringHttpMessageConverter.setWriteAcceptCharset(false);
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
- }
-
- /**
- * Set the default character set to use for reading and writing form data when
- * the request or response Content-Type header does not explicitly specify it.
- * <p>By default this is set to "UTF-8".
- */
- public void setCharset(Charset charset) {
- this.charset = charset;
+ applyDefaultCharset();
}
- /**
- * Set the character set to use when writing multipart data to encode file
- * names. Encoding is based on the encoded-word syntax defined in RFC 2047
- * and relies on {@code MimeUtility} from "javax.mail".
- * <p>If not set file names will be encoded as US-ASCII.
- * @param multipartCharset the charset to use
- * @since 4.1.1
- * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
- */
- public void setMultipartCharset(Charset multipartCharset) {
- this.multipartCharset = multipartCharset;
- }
/**
* Set the list of {@link MediaType} objects supported by this converter.
@@ -163,6 +144,49 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
this.partConverters.add(partConverter);
}
+ /**
+ * Set the default character set to use for reading and writing form data when
+ * the request or response Content-Type header does not explicitly specify it.
+ * <p>By default this is set to "UTF-8". As of 4.3, it will also be used as
+ * the default charset for the conversion of text bodies in a multipart request.
+ * In contrast to this, {@link #setMultipartCharset} only affects the encoding of
+ * <i>file names</i> in a multipart request according to the encoded-word syntax.
+ */
+ public void setCharset(Charset charset) {
+ if (charset != this.charset) {
+ this.charset = (charset != null ? charset : DEFAULT_CHARSET);
+ applyDefaultCharset();
+ }
+ }
+
+ /**
+ * Apply the configured charset as a default to registered part converters.
+ */
+ private void applyDefaultCharset() {
+ for (HttpMessageConverter<?> candidate : this.partConverters) {
+ if (candidate instanceof AbstractHttpMessageConverter) {
+ AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
+ // Only override default charset if the converter operates with a charset to begin with...
+ if (converter.getDefaultCharset() != null) {
+ converter.setDefaultCharset(this.charset);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the character set to use when writing multipart data to encode file
+ * names. Encoding is based on the encoded-word syntax defined in RFC 2047
+ * and relies on {@code MimeUtility} from "javax.mail".
+ * <p>If not set file names will be encoded as US-ASCII.
+ * @param multipartCharset the charset to use
+ * @since 4.1.1
+ * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
+ */
+ public void setMultipartCharset(Charset multipartCharset) {
+ this.multipartCharset = multipartCharset;
+ }
+
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
@@ -202,7 +226,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
- Charset charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset);
+ Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
@@ -255,7 +279,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
Charset charset;
if (contentType != null) {
outputMessage.getHeaders().setContentType(contentType);
- charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset);
+ charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
}
else {
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
diff --git a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java
index 5fab4fec..2e01e5eb 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java
@@ -71,7 +71,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve
* @param defaultCharset the default charset
*/
public ObjectToStringHttpMessageConverter(ConversionService conversionService, Charset defaultCharset) {
- super(new MediaType("text", "plain", defaultCharset));
+ super(defaultCharset, MediaType.TEXT_PLAIN);
Assert.notNull(conversionService, "ConversionService is required");
this.conversionService = conversionService;
diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java
index cc8da360..93bd3a3a 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,6 +16,7 @@
package org.springframework.http.converter;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import javax.activation.FileTypeMap;
@@ -33,12 +34,14 @@ import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
- * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}.
+ * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}
+ * and supports byte range requests.
*
* <p>By default, this converter can read all media types. The Java Activation Framework (JAF) -
* if available - is used to determine the {@code Content-Type} of written resources.
* If JAF is not available, {@code application/octet-stream} is used.
*
+ *
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Kazuki Shimizu
@@ -64,7 +67,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
- if (InputStreamResource.class == clazz){
+ if (InputStreamResource.class == clazz) {
return new InputStreamResource(inputMessage.getBody());
}
else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
@@ -90,25 +93,42 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
protected Long getContentLength(Resource resource, MediaType contentType) throws IOException {
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
- return (InputStreamResource.class == resource.getClass() ? null : resource.contentLength());
+ if (InputStreamResource.class == resource.getClass()) {
+ return null;
+ }
+ long contentLength = resource.contentLength();
+ return (contentLength < 0 ? null : contentLength);
}
@Override
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
- InputStream in = resource.getInputStream();
+ writeContent(resource, outputMessage);
+ }
+
+ protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException {
try {
- StreamUtils.copy(in, outputMessage.getBody());
- }
- finally {
+ InputStream in = resource.getInputStream();
try {
- in.close();
+ StreamUtils.copy(in, outputMessage.getBody());
+ }
+ catch (NullPointerException ex) {
+ // ignore, see SPR-13620
}
- catch (IOException ex) {
+ finally {
+ try {
+ in.close();
+ }
+ catch (Throwable ex) {
+ // ignore, see SPR-12999
+ }
}
}
- outputMessage.getBody().flush();
+ catch (FileNotFoundException ex) {
+ // ignore, see SPR-12999
+ }
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java
new file mode 100644
index 00000000..5c12092e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2002-2016 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.converter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+
+import org.springframework.core.io.support.ResourceRegion;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.util.Assert;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.util.StreamUtils;
+
+/**
+ * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion},
+ * or Collections of {@link ResourceRegion ResourceRegions}.
+ *
+ * @author Brian Clozel
+ * @since 4.3
+ */
+public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
+
+ public ResourceRegionHttpMessageConverter() {
+ super(MediaType.ALL);
+ }
+
+
+ @Override
+ protected boolean supports(Class<?> clazz) {
+ // should not be called as we override canRead/canWrite
+ return false;
+ }
+
+ @Override
+ public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
+ return false;
+ }
+
+ @Override
+ public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
+ throws IOException, HttpMessageNotReadableException {
+
+ return null;
+ }
+
+ @Override
+ protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)
+ throws IOException, HttpMessageNotReadableException {
+
+ return null;
+ }
+
+ @Override
+ public boolean canWrite(Class<?> clazz, MediaType mediaType) {
+ return canWrite(clazz, null, mediaType);
+ }
+
+ @Override
+ public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
+ if (!(type instanceof ParameterizedType)) {
+ return ResourceRegion.class.isAssignableFrom((Class) type);
+ }
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ if (!(parameterizedType.getRawType() instanceof Class)) {
+ return false;
+ }
+ Class<?> rawType = (Class<?>) parameterizedType.getRawType();
+ if (!(Collection.class.isAssignableFrom(rawType))) {
+ return false;
+ }
+ if (parameterizedType.getActualTypeArguments().length != 1) {
+ return false;
+ }
+ Type typeArgument = parameterizedType.getActualTypeArguments()[0];
+ if (!(typeArgument instanceof Class)) {
+ return false;
+ }
+ Class<?> typeArgumentClass = (Class<?>) typeArgument;
+ return typeArgumentClass.isAssignableFrom(ResourceRegion.class);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException {
+
+ if (object instanceof ResourceRegion) {
+ writeResourceRegion((ResourceRegion) object, outputMessage);
+ }
+ else {
+ Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object;
+ if(regions.size() == 1) {
+ writeResourceRegion(regions.iterator().next(), outputMessage);
+ }
+ else {
+ writeResourceRegionCollection((Collection<ResourceRegion>) object, outputMessage);
+ }
+ }
+ }
+
+ protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException {
+ Assert.notNull(region, "ResourceRegion must not be null");
+ HttpHeaders responseHeaders = outputMessage.getHeaders();
+ long start = region.getPosition();
+ long end = start + region.getCount() - 1;
+ Long resourceLength = region.getResource().contentLength();
+ end = Math.min(end, resourceLength - 1);
+ long rangeLength = end - start + 1;
+ responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + resourceLength);
+ responseHeaders.setContentLength(rangeLength);
+ InputStream in = region.getResource().getInputStream();
+ try {
+ StreamUtils.copyRange(in, outputMessage.getBody(), start, end);
+ }
+ finally {
+ try {
+ in.close();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+ }
+
+ private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions,
+ HttpOutputMessage outputMessage) throws IOException {
+
+ Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null");
+ HttpHeaders responseHeaders = outputMessage.getHeaders();
+ MediaType contentType = responseHeaders.getContentType();
+ String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
+ responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString);
+ OutputStream out = outputMessage.getBody();
+ for (ResourceRegion region : resourceRegions) {
+ long start = region.getPosition();
+ long end = start + region.getCount() - 1;
+ InputStream in = region.getResource().getInputStream();
+ // Writing MIME header.
+ println(out);
+ print(out, "--" + boundaryString);
+ println(out);
+ if (contentType != null) {
+ print(out, "Content-Type: " + contentType.toString());
+ println(out);
+ }
+ Long resourceLength = region.getResource().contentLength();
+ end = Math.min(end, resourceLength - 1);
+ print(out, "Content-Range: bytes " + start + "-" + end + "/" + resourceLength);
+ println(out);
+ println(out);
+ // Printing content
+ StreamUtils.copyRange(in, out, start, end);
+ }
+ println(out);
+ print(out, "--" + boundaryString + "--");
+ }
+
+
+
+ private static void println(OutputStream os) throws IOException {
+ os.write('\r');
+ os.write('\n');
+ }
+
+ private static void print(OutputStream os, String buf) throws IOException {
+ os.write(buf.getBytes("US-ASCII"));
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java
index 15c2693c..dc150054 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -35,6 +35,7 @@ import org.springframework.util.StreamUtils;
* by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
*
* @author Arjen Poutsma
+ * @author Juergen Hoeller
* @since 3.0
*/
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
@@ -42,8 +43,6 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
- private final Charset defaultCharset;
-
private final List<Charset> availableCharsets;
private boolean writeAcceptCharset = true;
@@ -62,8 +61,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
* type does not specify one.
*/
public StringHttpMessageConverter(Charset defaultCharset) {
- super(new MediaType("text", "plain", defaultCharset), MediaType.ALL);
- this.defaultCharset = defaultCharset;
+ super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
}
@@ -121,11 +119,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
}
private Charset getContentTypeCharset(MediaType contentType) {
- if (contentType != null && contentType.getCharSet() != null) {
- return contentType.getCharSet();
+ if (contentType != null && contentType.getCharset() != null) {
+ return contentType.getCharset();
}
else {
- return this.defaultCharset;
+ return getDefaultCharset();
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java
index 834fc820..9daa030f 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java
@@ -66,7 +66,7 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed> e
WireFeedInput feedInput = new WireFeedInput();
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset =
- (contentType != null && contentType.getCharSet() != null? contentType.getCharSet() : DEFAULT_CHARSET);
+ (contentType != null && contentType.getCharset() != null? contentType.getCharset() : DEFAULT_CHARSET);
try {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return (T) feedInput.build(reader);
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
index f29dc806..2d3dd80e 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java
@@ -17,22 +17,25 @@
package org.springframework.http.converter.json;
import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.core.ResolvableType;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
@@ -41,14 +44,13 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
import org.springframework.util.TypeUtils;
/**
* Abstract base class for Jackson based and content type independent
* {@link HttpMessageConverter} implementations.
*
- * <p>Compatible with Jackson 2.1 to 2.6.
+ * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
*
* @author Arjen Poutsma
* @author Keith Donald
@@ -61,14 +63,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
- // Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference
- private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class,
- "canDeserialize", JavaType.class, AtomicReference.class);
-
- // Check for Jackson 2.6+ for support of generic type aware serialization of polymorphic collections
- private static final boolean jackson26Available = ClassUtils.hasMethod(ObjectMapper.class,
- "setDefaultPrettyPrinter", PrettyPrinter.class);
-
protected ObjectMapper objectMapper;
@@ -77,16 +71,19 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
+ setDefaultCharset(DEFAULT_CHARSET);
}
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
super(supportedMediaType);
this.objectMapper = objectMapper;
+ setDefaultCharset(DEFAULT_CHARSET);
}
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
super(supportedMediaTypes);
this.objectMapper = objectMapper;
+ setDefaultCharset(DEFAULT_CHARSET);
}
@@ -146,23 +143,14 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return false;
}
JavaType javaType = getJavaType(type, contextClass);
- if (!jackson23Available || !logger.isWarnEnabled()) {
+ if (!logger.isWarnEnabled()) {
return this.objectMapper.canDeserialize(javaType);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canDeserialize(javaType, causeRef)) {
return true;
}
- Throwable cause = causeRef.get();
- if (cause != null) {
- String msg = "Failed to evaluate Jackson deserialization for type " + javaType;
- if (logger.isDebugEnabled()) {
- logger.warn(msg, cause);
- }
- else {
- logger.warn(msg + ": " + cause);
- }
- }
+ logWarningIfNecessary(javaType, causeRef.get());
return false;
}
@@ -171,16 +159,29 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
if (!canWrite(mediaType)) {
return false;
}
- if (!jackson23Available || !logger.isWarnEnabled()) {
+ if (!logger.isWarnEnabled()) {
return this.objectMapper.canSerialize(clazz);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
}
- Throwable cause = causeRef.get();
- if (cause != null) {
- String msg = "Failed to evaluate Jackson serialization for type [" + clazz + "]";
+ logWarningIfNecessary(clazz, causeRef.get());
+ return false;
+ }
+
+ /**
+ * Determine whether to log the given exception coming from a
+ * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
+ * @param type the class that Jackson tested for (de-)serializability
+ * @param cause the Jackson-thrown exception to evaluate
+ * (typically a {@link JsonMappingException})
+ * @since 4.3
+ */
+ protected void logWarningIfNecessary(Type type, Throwable cause) {
+ if (cause != null && !(cause instanceof JsonMappingException && cause.getMessage().startsWith("Can not find"))) {
+ String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") +
+ "serialization for type [" + type + "]";
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
@@ -188,7 +189,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
logger.warn(msg + ": " + cause);
}
}
- return false;
}
@Override
@@ -213,13 +213,12 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return readJavaType(javaType, inputMessage);
}
- @SuppressWarnings("deprecation")
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
- return this.objectMapper.readerWithView(deserializationView).withType(javaType).
+ return this.objectMapper.readerWithView(deserializationView).forType(javaType).
readValue(inputMessage.getBody());
}
}
@@ -231,7 +230,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
}
@Override
- @SuppressWarnings("deprecation")
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
@@ -250,7 +248,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
serializationView = container.getSerializationView();
filters = container.getFilters();
}
- if (jackson26Available && type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
+ if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter;
@@ -264,7 +262,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
objectWriter = this.objectMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
- objectWriter = objectWriter.withType(javaType);
+ objectWriter = objectWriter.forType(javaType);
}
objectWriter.writeValue(generator, value);
@@ -313,10 +311,62 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
* @return the Jackson JavaType
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
- TypeFactory tf = this.objectMapper.getTypeFactory();
- // Conditional call because Jackson 2.7 does not support null contextClass anymore
- // TypeVariable resolution will not work with Jackson 2.7, see SPR-13853 for more details
- return (contextClass != null ? tf.constructType(type, contextClass) : tf.constructType(type));
+ TypeFactory typeFactory = this.objectMapper.getTypeFactory();
+ if (contextClass != null) {
+ ResolvableType resolvedType = ResolvableType.forType(type);
+ if (type instanceof TypeVariable) {
+ ResolvableType resolvedTypeVariable = resolveVariable(
+ (TypeVariable<?>) type, ResolvableType.forClass(contextClass));
+ if (resolvedTypeVariable != ResolvableType.NONE) {
+ return typeFactory.constructType(resolvedTypeVariable.resolve());
+ }
+ }
+ else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
+ Type[] typeArguments = parameterizedType.getActualTypeArguments();
+ for (int i = 0; i < typeArguments.length; i++) {
+ Type typeArgument = typeArguments[i];
+ if (typeArgument instanceof TypeVariable) {
+ ResolvableType resolvedTypeArgument = resolveVariable(
+ (TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
+ if (resolvedTypeArgument != ResolvableType.NONE) {
+ generics[i] = resolvedTypeArgument.resolve();
+ }
+ else {
+ generics[i] = ResolvableType.forType(typeArgument).resolve();
+ }
+ }
+ else {
+ generics[i] = ResolvableType.forType(typeArgument).resolve();
+ }
+ }
+ return typeFactory.constructType(ResolvableType.
+ forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
+ }
+ }
+ return typeFactory.constructType(type);
+ }
+
+ private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
+ ResolvableType resolvedType;
+ if (contextType.hasGenerics()) {
+ resolvedType = ResolvableType.forType(typeVariable, contextType);
+ if (resolvedType.resolve() != null) {
+ return resolvedType;
+ }
+ }
+ resolvedType = resolveVariable(typeVariable, contextType.getSuperType());
+ if (resolvedType.resolve() != null) {
+ return resolvedType;
+ }
+ for (ResolvableType ifc : contextType.getInterfaces()) {
+ resolvedType = resolveVariable(typeVariable, ifc);
+ if (resolvedType.resolve() != null) {
+ return resolvedType;
+ }
+ }
+ return ResolvableType.NONE;
}
/**
@@ -325,8 +375,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
* @return the JSON encoding to use (never {@code null})
*/
protected JsonEncoding getJsonEncoding(MediaType contentType) {
- if (contentType != null && contentType.getCharSet() != null) {
- Charset charset = contentType.getCharSet();
+ if (contentType != null && contentType.getCharset() != null) {
+ Charset charset = contentType.getCharset();
for (JsonEncoding encoding : JsonEncoding.values()) {
if (charset.name().equals(encoding.getJavaName())) {
return encoding;
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java
index 38f7f432..d82bd28d 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java
@@ -68,7 +68,8 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte
* Construct a new {@code GsonHttpMessageConverter}.
*/
public GsonHttpMessageConverter() {
- super(MediaType.APPLICATION_JSON_UTF8, new MediaType("application", "*+json", DEFAULT_CHARSET));
+ super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
+ this.setDefaultCharset(DEFAULT_CHARSET);
}
@@ -177,10 +178,10 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte
}
private Charset getCharset(HttpHeaders headers) {
- if (headers == null || headers.getContentType() == null || headers.getContentType().getCharSet() == null) {
+ if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) {
return DEFAULT_CHARSET;
}
- return headers.getContentType().getCharSet();
+ return headers.getContentType().getCharset();
}
@Override
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java
index b2ee1e5b..046a3b1e 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -47,6 +47,8 @@ import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.FilterProvider;
+import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
+import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.beans.BeanUtils;
@@ -73,9 +75,10 @@ import org.springframework.util.StringUtils;
* <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>: support for other Java 8 types like {@link java.util.Optional}</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>: support for Java 8 Date & Time API types</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-joda">jackson-datatype-joda</a>: support for Joda-Time types</li>
+ * <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>: support for Kotlin classes and data classes</li>
* </ul>
*
- * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher.
+ * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
*
* @author Sebastien Deleuze
* @author Juergen Hoeller
@@ -127,6 +130,8 @@ public class Jackson2ObjectMapperBuilder {
private ApplicationContext applicationContext;
+ private Boolean defaultUseWrapper;
+
/**
* If set to {@code true}, an {@link XmlMapper} will be created using its
@@ -279,8 +284,7 @@ public class Jackson2ObjectMapperBuilder {
/**
* Configure custom serializers. Each serializer is registered for the type
- * returned by {@link JsonSerializer#handledType()}, which must not be
- * {@code null}.
+ * returned by {@link JsonSerializer#handledType()}, which must not be {@code null}.
* @see #serializersByType(Map)
*/
public Jackson2ObjectMapperBuilder serializers(JsonSerializer<?>... serializers) {
@@ -320,6 +324,25 @@ public class Jackson2ObjectMapperBuilder {
}
/**
+ * Configure custom deserializers. Each deserializer is registered for the type
+ * returned by {@link JsonDeserializer#handledType()}, which must not be {@code null}.
+ * @since 4.3
+ * @see #deserializersByType(Map)
+ */
+ public Jackson2ObjectMapperBuilder deserializers(JsonDeserializer<?>... deserializers) {
+ if (deserializers != null) {
+ for (JsonDeserializer<?> deserializer : deserializers) {
+ Class<?> handledType = deserializer.handledType();
+ if (handledType == null || handledType == Object.class) {
+ throw new IllegalArgumentException("Unknown handled type in " + deserializer.getClass().getName());
+ }
+ this.deserializers.put(deserializer.handledType(), deserializer);
+ }
+ }
+ return this;
+ }
+
+ /**
* Configure a custom deserializer for the given type.
* @since 4.1.2
*/
@@ -393,6 +416,16 @@ public class Jackson2ObjectMapperBuilder {
}
/**
+ * Define if a wrapper will be used for indexed (List, array) properties or not by
+ * default (only applies to {@link XmlMapper}).
+ * @since 4.3
+ */
+ public Jackson2ObjectMapperBuilder defaultUseWrapper(boolean defaultUseWrapper) {
+ this.defaultUseWrapper = defaultUseWrapper;
+ return this;
+ }
+
+ /**
* Specify features to enable.
* @see com.fasterxml.jackson.core.JsonParser.Feature
* @see com.fasterxml.jackson.core.JsonGenerator.Feature
@@ -547,7 +580,9 @@ public class Jackson2ObjectMapperBuilder {
public <T extends ObjectMapper> T build() {
ObjectMapper mapper;
if (this.createXmlMapper) {
- mapper = new XmlObjectMapperInitializer().create();
+ mapper = (this.defaultUseWrapper != null ?
+ new XmlObjectMapperInitializer().create(this.defaultUseWrapper) :
+ new XmlObjectMapperInitializer().create());
}
else {
mapper = new ObjectMapper();
@@ -561,7 +596,6 @@ public class Jackson2ObjectMapperBuilder {
* settings. This can be applied to any number of {@code ObjectMappers}.
* @param objectMapper the ObjectMapper to configure
*/
- @SuppressWarnings("deprecation")
public void configure(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
@@ -609,13 +643,11 @@ public class Jackson2ObjectMapperBuilder {
}
if (this.filters != null) {
- // Deprecated as of Jackson 2.6, but just in favor of a fluent variant.
- objectMapper.setFilters(this.filters);
+ objectMapper.setFilterProvider(this.filters);
}
for (Class<?> target : this.mixIns.keySet()) {
- // Deprecated as of Jackson 2.5, but just in favor of a fluent variant.
- objectMapper.addMixInAnnotations(target, this.mixIns.get(target));
+ objectMapper.addMixIn(target, this.mixIns.get(target));
}
if (!this.serializers.isEmpty() || !this.deserializers.isEmpty()) {
@@ -693,7 +725,7 @@ public class Jackson2ObjectMapperBuilder {
try {
Class<? extends Module> jdk7Module = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.datatype.jdk7.Jdk7Module", this.moduleClassLoader);
- objectMapper.registerModule(BeanUtils.instantiate(jdk7Module));
+ objectMapper.registerModule(BeanUtils.instantiateClass(jdk7Module));
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jdk7 not available
@@ -705,7 +737,7 @@ public class Jackson2ObjectMapperBuilder {
try {
Class<? extends Module> jdk8Module = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader);
- objectMapper.registerModule(BeanUtils.instantiate(jdk8Module));
+ objectMapper.registerModule(BeanUtils.instantiateClass(jdk8Module));
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jdk8 not available
@@ -717,18 +749,10 @@ public class Jackson2ObjectMapperBuilder {
try {
Class<? extends Module> javaTimeModule = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader);
- objectMapper.registerModule(BeanUtils.instantiate(javaTimeModule));
+ objectMapper.registerModule(BeanUtils.instantiateClass(javaTimeModule));
}
catch (ClassNotFoundException ex) {
- // jackson-datatype-jsr310 not available or older than 2.6
- try {
- Class<? extends Module> jsr310Module = (Class<? extends Module>)
- ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JSR310Module", this.moduleClassLoader);
- objectMapper.registerModule(BeanUtils.instantiate(jsr310Module));
- }
- catch (ClassNotFoundException ex2) {
- // OK, jackson-datatype-jsr310 not available at all...
- }
+ // jackson-datatype-jsr310 not available
}
}
@@ -737,12 +761,24 @@ public class Jackson2ObjectMapperBuilder {
try {
Class<? extends Module> jodaModule = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", this.moduleClassLoader);
- objectMapper.registerModule(BeanUtils.instantiate(jodaModule));
+ objectMapper.registerModule(BeanUtils.instantiateClass(jodaModule));
}
catch (ClassNotFoundException ex) {
// jackson-datatype-joda not available
}
}
+
+ // Kotlin present?
+ if (ClassUtils.isPresent("kotlin.Unit", this.moduleClassLoader)) {
+ try {
+ Class<? extends Module> kotlinModule = (Class<? extends Module>)
+ ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader);
+ objectMapper.registerModule(BeanUtils.instantiateClass(kotlinModule));
+ }
+ catch (ClassNotFoundException ex) {
+ // jackson-module-kotlin not available
+ }
+ }
}
@@ -768,11 +804,21 @@ public class Jackson2ObjectMapperBuilder {
private static class XmlObjectMapperInitializer {
public ObjectMapper create() {
+ return new XmlMapper(xmlInputFactory());
+ }
+
+ public ObjectMapper create(boolean defaultUseWrapper) {
+ JacksonXmlModule module = new JacksonXmlModule();
+ module.setDefaultUseWrapper(defaultUseWrapper);
+ return new XmlMapper(new XmlFactory(xmlInputFactory()), module);
+ }
+
+ private static XMLInputFactory xmlInputFactory() {
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
- return new XmlMapper(inputFactory);
+ return inputFactory;
}
private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() {
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java
index f17016e9..d945a7fb 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -116,6 +116,7 @@ import org.springframework.context.ApplicationContextAware;
* <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>: support for other Java 8 types like {@link java.util.Optional}</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>: support for Java 8 Date & Time API types</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-joda">jackson-datatype-joda</a>: support for Joda-Time types</li>
+ * <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>: support for Kotlin classes and data classes</li>
* </ul>
*
* <p>In case you want to configure Jackson's {@link ObjectMapper} with a custom {@link Module},
@@ -127,7 +128,7 @@ import org.springframework.context.ApplicationContextAware;
* &lt;/bean
* </pre>
*
- * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher.
+ * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
*
* @author <a href="mailto:dmitry.katsubo@gmail.com">Dmitry Katsubo</a>
* @author Rossen Stoyanchev
@@ -255,8 +256,7 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
/**
* Configure custom serializers. Each serializer is registered for the type
- * returned by {@link JsonSerializer#handledType()}, which must not be
- * {@code null}.
+ * returned by {@link JsonSerializer#handledType()}, which must not be {@code null}.
* @see #setSerializersByType(Map)
*/
public void setSerializers(JsonSerializer<?>... serializers) {
@@ -272,6 +272,16 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
}
/**
+ * Configure custom deserializers. Each deserializer is registered for the type
+ * returned by {@link JsonDeserializer#handledType()}, which must not be {@code null}.
+ * @since 4.3
+ * @see #setDeserializersByType(Map)
+ */
+ public void setDeserializers(JsonDeserializer<?>... deserializers) {
+ this.builder.deserializers(deserializers);
+ }
+
+ /**
* Configure custom deserializers for the given types.
*/
public void setDeserializersByType(Map<Class<?>, JsonDeserializer<?>> deserializers) {
@@ -325,6 +335,15 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
}
/**
+ * Define if a wrapper will be used for indexed (List, array) properties or not by
+ * default (only applies to {@link XmlMapper}).
+ * @since 4.3
+ */
+ public void setDefaultUseWrapper(boolean defaultUseWrapper) {
+ this.builder.defaultUseWrapper(defaultUseWrapper);
+ }
+
+ /**
* Specify features to enable.
* @see com.fasterxml.jackson.core.JsonParser.Feature
* @see com.fasterxml.jackson.core.JsonGenerator.Feature
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
index e2ef12b9..4832ec0b 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
@@ -35,7 +35,7 @@ import org.springframework.http.MediaType;
*
* <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}.
*
- * <p>Compatible with Jackson 2.1 to 2.6.
+ * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
*
* @author Arjen Poutsma
* @author Keith Donald
@@ -63,8 +63,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMes
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
- super(objectMapper, MediaType.APPLICATION_JSON_UTF8,
- new MediaType("application", "*+json", DEFAULT_CHARSET));
+ super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
/**
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java b/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java
index f208c32a..ad4874c1 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java
@@ -16,16 +16,22 @@
package org.springframework.http.converter.json;
+import com.fasterxml.jackson.annotation.ObjectIdGenerator;
+import com.fasterxml.jackson.annotation.ObjectIdResolver;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
+import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter;
+import com.fasterxml.jackson.databind.util.Converter;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
@@ -36,6 +42,11 @@ import org.springframework.util.Assert;
* {@link KeyDeserializer}, {@link TypeResolverBuilder}, {@link TypeIdResolver})
* beans with autowiring against a Spring {@link ApplicationContext}.
*
+ * <p>As of Spring 4.3, this overrides all factory methods in {@link HandlerInstantiator},
+ * including non-abstract ones and recently introduced ones from Jackson 2.4 and 2.5:
+ * for {@link ValueInstantiator}, {@link ObjectIdGenerator}, {@link ObjectIdResolver},
+ * {@link PropertyNamingStrategy}, {@link Converter}, {@link VirtualBeanPropertyWriter}.
+ *
* @author Sebastien Deleuze
* @author Juergen Hoeller
* @since 4.1.3
@@ -83,4 +94,40 @@ public class SpringHandlerInstantiator extends HandlerInstantiator {
return (TypeIdResolver) this.beanFactory.createBean(implClass);
}
+ /** @since 4.3 */
+ @Override
+ public ValueInstantiator valueInstantiatorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+ return (ValueInstantiator) this.beanFactory.createBean(implClass);
+ }
+
+ /** @since 4.3 */
+ @Override
+ public ObjectIdGenerator<?> objectIdGeneratorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+ return (ObjectIdGenerator<?>) this.beanFactory.createBean(implClass);
+ }
+
+ /** @since 4.3 */
+ @Override
+ public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+ return (ObjectIdResolver) this.beanFactory.createBean(implClass);
+ }
+
+ /** @since 4.3 */
+ @Override
+ public PropertyNamingStrategy namingStrategyInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+ return (PropertyNamingStrategy) this.beanFactory.createBean(implClass);
+ }
+
+ /** @since 4.3 */
+ @Override
+ public Converter<?, ?> converterInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
+ return (Converter<?, ?>) this.beanFactory.createBean(implClass);
+ }
+
+ /** @since 4.3 */
+ @Override
+ public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig<?> config, Class<?> implClass) {
+ return (VirtualBeanPropertyWriter) this.beanFactory.createBean(implClass);
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java
index 388b5533..9f023882 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -28,6 +28,7 @@ import com.google.protobuf.Message;
import com.google.protobuf.TextFormat;
import com.googlecode.protobuf.format.HtmlFormat;
import com.googlecode.protobuf.format.JsonFormat;
+import com.googlecode.protobuf.format.ProtobufFormatter;
import com.googlecode.protobuf.format.XmlFormat;
import org.springframework.http.HttpInputMessage;
@@ -38,7 +39,6 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.FileCopyUtils;
-
/**
* An {@code HttpMessageConverter} that reads and writes {@link com.google.protobuf.Message}s
* using <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>.
@@ -48,8 +48,7 @@ import org.springframework.util.FileCopyUtils;
*
* <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary.
*
- * <p>Requires Protobuf 2.5/2.6 and Protobuf Java Format 1.2.
- * (Note: Does not work with later Protobuf Java Format versions in Spring 4.2 yet.)
+ * <p>Requires Protobuf 2.6 and Protobuf Java Format 1.4, as of Spring 4.3.
*
* @author Alex Antonov
* @author Brian Clozel
@@ -67,6 +66,13 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message";
+ private static final ProtobufFormatter JSON_FORMAT = new JsonFormat();
+
+ private static final ProtobufFormatter XML_FORMAT = new XmlFormat();
+
+ private static final ProtobufFormatter HTML_FORMAT = new HtmlFormat();
+
+
private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<Class<?>, Method>();
private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
@@ -109,7 +115,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
if (contentType == null) {
contentType = PROTOBUF;
}
- Charset charset = contentType.getCharSet();
+ Charset charset = contentType.getCharset();
if (charset == null) {
charset = DEFAULT_CHARSET;
}
@@ -121,12 +127,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
TextFormat.merge(reader, this.extensionRegistry, builder);
}
else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
- InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset);
- JsonFormat.merge(reader, this.extensionRegistry, builder);
+ JSON_FORMAT.merge(inputMessage.getBody(), charset, this.extensionRegistry, builder);
}
else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
- InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset);
- XmlFormat.merge(reader, this.extensionRegistry, builder);
+ XML_FORMAT.merge(inputMessage.getBody(), charset, this.extensionRegistry, builder);
}
else {
builder.mergeFrom(inputMessage.getBody(), this.extensionRegistry);
@@ -155,7 +159,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
if (contentType == null) {
contentType = getDefaultContentType(message);
}
- Charset charset = contentType.getCharSet();
+ Charset charset = contentType.getCharset();
if (charset == null) {
charset = DEFAULT_CHARSET;
}
@@ -166,19 +170,13 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
outputStreamWriter.flush();
}
else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
- OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
- JsonFormat.print(message, outputStreamWriter);
- outputStreamWriter.flush();
+ JSON_FORMAT.print(message, outputMessage.getBody(), charset);
}
else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
- OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
- XmlFormat.print(message, outputStreamWriter);
- outputStreamWriter.flush();
+ XML_FORMAT.print(message, outputMessage.getBody(), charset);
}
else if (MediaType.TEXT_HTML.isCompatibleWith(contentType)) {
- OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
- HtmlFormat.print(message, outputStreamWriter);
- outputStreamWriter.flush();
+ HTML_FORMAT.print(message, outputMessage.getBody(), charset);
}
else if (PROTOBUF.isCompatibleWith(contentType)) {
setProtoHeader(outputMessage, message);
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java
index 53773a48..5be9e392 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java
@@ -51,7 +51,7 @@ import org.springframework.util.ClassUtils;
* HttpMessageConverter} that can read and write XML using JAXB2.
*
* <p>This converter can read classes annotated with {@link XmlRootElement} and
- * {@link XmlType}, and write classes annotated with with {@link XmlRootElement},
+ * {@link XmlType}, and write classes annotated with {@link XmlRootElement},
* or subclasses thereof.
*
* <p>Note that if using Spring's Marshaller/Unmarshaller abstractions from the
@@ -195,8 +195,8 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
}
private void setCharset(MediaType contentType, Marshaller marshaller) throws PropertyException {
- if (contentType != null && contentType.getCharSet() != null) {
- marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharSet().name());
+ if (contentType != null && contentType.getCharset() != null) {
+ marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharset().name());
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java
index 03084759..39eb13ef 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java
@@ -35,7 +35,7 @@ import org.springframework.util.Assert;
*
* <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}.
*
- * <p>Compatible with Jackson 2.1 to 2.6.
+ * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
*
* @author Sebastien Deleuze
* @since 4.1
@@ -57,9 +57,9 @@ public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2Http
* @see Jackson2ObjectMapperBuilder#xml()
*/
public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) {
- super(objectMapper, new MediaType("application", "xml", DEFAULT_CHARSET),
- new MediaType("text", "xml", DEFAULT_CHARSET),
- new MediaType("application", "*+xml", DEFAULT_CHARSET));
+ super(objectMapper, new MediaType("application", "xml"),
+ new MediaType("text", "xml"),
+ new MediaType("application", "*+xml"));
Assert.isAssignable(XmlMapper.class, objectMapper.getClass());
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java
index e9875107..8b411259 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -175,9 +175,8 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
}
catch (NullPointerException ex) {
if (!isSupportDtd()) {
- throw new HttpMessageNotReadableException("NPE while unmarshalling. " +
- "This can happen on JDK 1.6 due to the presence of DTD " +
- "declarations, which are disabled.", ex);
+ throw new HttpMessageNotReadableException("NPE while unmarshalling: " +
+ "This can happen due to the presence of DTD declarations which are disabled.", ex);
}
throw ex;
}
@@ -191,14 +190,14 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private SAXSource readSAXSource(InputStream body) throws IOException {
try {
- XMLReader reader = XMLReaderFactory.createXMLReader();
- reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
- reader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
+ XMLReader xmlReader = XMLReaderFactory.createXMLReader();
+ xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
+ xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
if (!isProcessExternalEntities()) {
- reader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
+ xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
}
byte[] bytes = StreamUtils.copyToByteArray(body);
- return new SAXSource(reader, new InputSource(new ByteArrayInputStream(bytes)));
+ return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream(bytes)));
}
catch (SAXException ex) {
throw new HttpMessageNotReadableException("Could not parse document: " + ex.getMessage(), ex);
diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java
index 4a8f342a..660044b8 100644
--- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java
@@ -125,7 +125,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
this.headers.setContentType(contentType);
}
}
- if (contentType != null && contentType.getCharSet() == null) {
+ if (contentType != null && contentType.getCharset() == null) {
String requestEncoding = this.servletRequest.getCharacterEncoding();
if (StringUtils.hasLength(requestEncoding)) {
Charset charSet = Charset.forName(requestEncoding);
diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java
index 1aebe518..5944297f 100644
--- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -73,6 +73,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public void setStatusCode(HttpStatus status) {
+ Assert.notNull(status, "HttpStatus must not be null");
this.servletResponse.setStatus(status.value());
}
@@ -114,8 +115,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
this.servletResponse.setContentType(this.headers.getContentType().toString());
}
if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null &&
- this.headers.getContentType().getCharSet() != null) {
- this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name());
+ this.headers.getContentType().getCharset() != null) {
+ this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
}
this.headersWritten = true;
}
diff --git a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java
index 7b0924ba..7f78a822 100644
--- a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java
+++ b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -43,6 +43,7 @@ import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.remoting.support.RemoteInvocationResult;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -69,6 +70,21 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);
+
+ private static Class<?> abstractHttpClientClass;
+
+ static {
+ try {
+ // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3)
+ abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient",
+ HttpComponentsHttpInvokerRequestExecutor.class.getClassLoader());
+ }
+ catch (ClassNotFoundException ex) {
+ // Probably removed from HttpComponents in the meantime...
+ }
+ }
+
+
private HttpClient httpClient;
private RequestConfig requestConfig;
@@ -156,7 +172,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
*/
@SuppressWarnings("deprecation")
private void setLegacyConnectionTimeout(HttpClient client, int timeout) {
- if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) {
+ if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
}
}
@@ -198,7 +214,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
*/
@SuppressWarnings("deprecation")
private void setLegacySocketTimeout(HttpClient client, int timeout) {
- if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) {
+ if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout);
}
}
diff --git a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java
index bcbc09f9..82bb00b6 100644
--- a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java
+++ b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java
@@ -16,6 +16,7 @@
package org.springframework.remoting.httpinvoker;
+import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
@@ -174,7 +175,8 @@ public class HttpInvokerServiceExporter extends RemoteInvocationSerializingExpor
HttpServletRequest request, HttpServletResponse response, RemoteInvocationResult result, OutputStream os)
throws IOException {
- ObjectOutputStream oos = createObjectOutputStream(decorateOutputStream(request, response, os));
+ ObjectOutputStream oos =
+ createObjectOutputStream(new FlushGuardedOutputStream(decorateOutputStream(request, response, os)));
try {
doWriteRemoteInvocationResult(result, oos);
}
@@ -200,4 +202,28 @@ public class HttpInvokerServiceExporter extends RemoteInvocationSerializingExpor
return os;
}
+
+ /**
+ * Decorate an {@code OutputStream} to guard against {@code flush()} calls,
+ * which are turned into no-ops.
+ *
+ * <p>Because {@link ObjectOutputStream#close()} will in fact flush/drain
+ * the underlying stream twice, this {@link FilterOutputStream} will
+ * guard against individual flush calls. Multiple flush calls can lead
+ * to performance issues, since writes aren't gathered as they should be.
+ *
+ * @see <a href="https://jira.spring.io/browse/SPR-14040">SPR-14040</a>
+ */
+ private static class FlushGuardedOutputStream extends FilterOutputStream {
+
+ public FlushGuardedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ // Do nothing on flush
+ }
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java b/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java
index 08c47e61..8746786f 100644
--- a/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java
+++ b/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -31,9 +31,9 @@ import javax.xml.ws.WebServiceProvider;
* <p>Note that this exporter will only work if the JAX-WS runtime actually
* supports publishing with an address argument, i.e. if the JAX-WS runtime
* ships an internal HTTP server. This is the case with the JAX-WS runtime
- * that's inclued in Sun's JDK 1.6 but not with the standalone JAX-WS 2.1 RI.
+ * that's included in Sun's JDK 6 but not with the standalone JAX-WS 2.1 RI.
*
- * <p>For explicit configuration of JAX-WS endpoints with Sun's JDK 1.6
+ * <p>For explicit configuration of JAX-WS endpoints with Sun's JDK 6
* HTTP server, consider using {@link SimpleHttpServerJaxWsServiceExporter}!
*
* @author Juergen Hoeller
diff --git a/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java b/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java
index 448bd4ed..d17f8d39 100644
--- a/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java
+++ b/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -27,6 +27,9 @@ import javax.servlet.ServletException;
@SuppressWarnings("serial")
public class HttpSessionRequiredException extends ServletException {
+ private String expectedAttribute;
+
+
/**
* Create a new HttpSessionRequiredException.
* @param msg the detail message
@@ -35,4 +38,24 @@ public class HttpSessionRequiredException extends ServletException {
super(msg);
}
+ /**
+ * Create a new HttpSessionRequiredException.
+ * @param msg the detail message
+ * @param expectedAttribute the name of the expected session attribute
+ * @since 4.3
+ */
+ public HttpSessionRequiredException(String msg, String expectedAttribute) {
+ super(msg);
+ this.expectedAttribute = expectedAttribute;
+ }
+
+
+ /**
+ * Return the name of the expected session attribute, if any.
+ * @since 4.3
+ */
+ public String getExpectedAttribute() {
+ return this.expectedAttribute;
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java
index 3f6efab3..81553b81 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java
@@ -93,6 +93,22 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
}
/**
+ * Find a {@code ContentNegotiationStrategy} of the given type.
+ * @param strategyType the strategy type
+ * @return the first matching strategy or {@code null}.
+ * @since 4.3
+ */
+ @SuppressWarnings("unchecked")
+ public <T extends ContentNegotiationStrategy> T getStrategy(Class<T> strategyType) {
+ for (ContentNegotiationStrategy strategy : getStrategies()) {
+ if (strategyType.isInstance(strategy)) {
+ return (T) strategy;
+ }
+ }
+ return null;
+ }
+
+ /**
* Register more {@code MediaTypeFileExtensionResolver} instances in addition
* to those detected at construction.
* @param resolvers the resolvers to add
diff --git a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java
index 2642853d..89dd23a2 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java
@@ -16,13 +16,13 @@
package org.springframework.web.accept;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
-import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
@@ -30,6 +30,7 @@ import org.springframework.web.context.request.NativeWebRequest;
* A {@code ContentNegotiationStrategy} that checks the 'Accept' request header.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.2
*/
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
@@ -42,18 +43,20 @@ public class HeaderContentNegotiationStrategy implements ContentNegotiationStrat
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
- String header = request.getHeader(HttpHeaders.ACCEPT);
- if (!StringUtils.hasText(header)) {
- return Collections.emptyList();
+ String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
+ if (headerValueArray == null) {
+ return Collections.<MediaType>emptyList();
}
+
+ List<String> headerValues = Arrays.asList(headerValueArray);
try {
- List<MediaType> mediaTypes = MediaType.parseMediaTypes(header);
+ List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return mediaTypes;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
- "Could not parse 'Accept' header [" + header + "]: " + ex.getMessage());
+ "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java
index 9f0c2672..000b6379 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java
@@ -30,12 +30,13 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
+import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UrlPathHelper;
-import org.springframework.web.util.WebUtils;
/**
* A {@code ContentNegotiationStrategy} that resolves the file extension in the
@@ -51,20 +52,14 @@ import org.springframework.web.util.WebUtils;
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class PathExtensionContentNegotiationStrategy
- extends AbstractMappingContentNegotiationStrategy {
-
- private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class);
+public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap",
PathExtensionContentNegotiationStrategy.class.getClassLoader());
- private static final UrlPathHelper PATH_HELPER = new UrlPathHelper();
-
- static {
- PATH_HELPER.setUrlDecode(false);
- }
+ private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class);
+ private UrlPathHelper urlPathHelper = new UrlPathHelper();
private boolean useJaf = true;
@@ -72,21 +67,31 @@ public class PathExtensionContentNegotiationStrategy
/**
+ * Create an instance without any mappings to start with. Mappings may be added
+ * later on if any extensions are resolved through the Java Activation framework.
+ */
+ public PathExtensionContentNegotiationStrategy() {
+ this(null);
+ }
+
+ /**
* Create an instance with the given map of file extensions and media types.
*/
public PathExtensionContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes);
+ this.urlPathHelper.setUrlDecode(false);
}
+
/**
- * Create an instance without any mappings to start with. Mappings may be added
- * later on if any extensions are resolved through the Java Activation framework.
+ * Configure a {@code UrlPathHelper} to use in {@link #getMediaTypeKey}
+ * in order to derive the lookup path for a target request URL path.
+ * @since 4.2.8
*/
- public PathExtensionContentNegotiationStrategy() {
- super(null);
+ public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
+ this.urlPathHelper = urlPathHelper;
}
-
/**
* Whether to use the Java Activation Framework to look up file extensions.
* <p>By default this is set to "true" but depends on JAF being present.
@@ -112,10 +117,9 @@ public class PathExtensionContentNegotiationStrategy
logger.warn("An HttpServletRequest is required to determine the media type key");
return null;
}
- String path = PATH_HELPER.getLookupPathForRequest(request);
- String filename = WebUtils.extractFullFilenameFromUrlPath(path);
- String extension = StringUtils.getFilenameExtension(filename);
- return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null;
+ String path = this.urlPathHelper.getLookupPathForRequest(request);
+ String extension = UriUtils.extractFileExtension(path);
+ return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null);
}
@Override
@@ -123,7 +127,7 @@ public class PathExtensionContentNegotiationStrategy
throws HttpMediaTypeNotAcceptableException {
if (this.useJaf && JAF_PRESENT) {
- MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + extension);
+ MediaType mediaType = ActivationMediaTypeFactory.getMediaType("file." + extension);
if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
return mediaType;
}
@@ -134,11 +138,37 @@ public class PathExtensionContentNegotiationStrategy
throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
}
+ /**
+ * A public method exposing the knowledge of the path extension strategy to
+ * resolve file extensions to a MediaType in this case for a given
+ * {@link Resource}. The method first looks up any explicitly registered
+ * file extensions first and then falls back on JAF if available.
+ * @param resource the resource to look up
+ * @return the MediaType for the extension or {@code null}.
+ * @since 4.3
+ */
+ public MediaType getMediaTypeForResource(Resource resource) {
+ Assert.notNull(resource);
+ MediaType mediaType = null;
+ String filename = resource.getFilename();
+ String extension = StringUtils.getFilenameExtension(filename);
+ if (extension != null) {
+ mediaType = lookupMediaType(extension);
+ }
+ if (mediaType == null && JAF_PRESENT) {
+ mediaType = ActivationMediaTypeFactory.getMediaType(filename);
+ }
+ if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
+ mediaType = null;
+ }
+ return mediaType;
+ }
+
/**
* Inner class to avoid hard-coded dependency on JAF.
*/
- private static class JafMediaTypeFactory {
+ private static class ActivationMediaTypeFactory {
private static final FileTypeMap fileTypeMap;
diff --git a/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java
index 615568b4..10d80b2b 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java
@@ -19,6 +19,7 @@ package org.springframework.web.accept;
import java.util.Map;
import javax.servlet.ServletContext;
+import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -82,4 +83,29 @@ public class ServletPathExtensionContentNegotiationStrategy extends PathExtensio
return mediaType;
}
+ /**
+ * Extends the base class
+ * {@link PathExtensionContentNegotiationStrategy#getMediaTypeForResource}
+ * with the ability to also look up through the ServletContext.
+ * @param resource the resource to look up
+ * @return the MediaType for the extension or {@code null}.
+ * @since 4.3
+ */
+ public MediaType getMediaTypeForResource(Resource resource) {
+ MediaType mediaType = null;
+ if (this.servletContext != null) {
+ String mimeType = this.servletContext.getMimeType(resource.getFilename());
+ if (StringUtils.hasText(mimeType)) {
+ mediaType = MediaType.parseMediaType(mimeType);
+ }
+ }
+ if (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
+ MediaType superMediaType = super.getMediaTypeForResource(resource);
+ if (superMediaType != null) {
+ mediaType = superMediaType;
+ }
+ }
+ return mediaType;
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java
index 77562398..442b9718 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -17,11 +17,13 @@
package org.springframework.web.bind;
import java.lang.reflect.Array;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
+import org.springframework.core.CollectionFactory;
import org.springframework.validation.DataBinder;
import org.springframework.web.multipart.MultipartFile;
@@ -40,6 +42,7 @@ import org.springframework.web.multipart.MultipartFile;
*
* @author Juergen Hoeller
* @author Scott Andrews
+ * @author Brian Clozel
* @since 1.2
* @see #registerCustomEditor
* @see #setAllowedFields
@@ -243,26 +246,41 @@ public class WebDataBinder extends DataBinder {
/**
* Determine an empty value for the specified field.
- * <p>Default implementation returns {@code Boolean.FALSE}
- * for boolean fields and an empty array of array types.
- * Else, {@code null} is used as default.
+ * <p>Default implementation returns:
+ * <ul>
+ * <li>{@code Boolean.FALSE} for boolean fields
+ * <li>an empty array for array types
+ * <li>Collection implementations for Collection types
+ * <li>Map implementations for Map types
+ * <li>else, {@code null} is used as default
+ * </ul>
* @param field the name of the field
* @param fieldType the type of the field
* @return the empty value (for most fields: null)
*/
protected Object getEmptyValue(String field, Class<?> fieldType) {
- if (fieldType != null && boolean.class == fieldType || Boolean.class == fieldType) {
- // Special handling of boolean property.
- return Boolean.FALSE;
- }
- else if (fieldType != null && fieldType.isArray()) {
- // Special handling of array property.
- return Array.newInstance(fieldType.getComponentType(), 0);
- }
- else {
- // Default value: try null.
- return null;
+ if (fieldType != null) {
+ try {
+ if (boolean.class == fieldType || Boolean.class == fieldType) {
+ // Special handling of boolean property.
+ return Boolean.FALSE;
+ }
+ else if (fieldType.isArray()) {
+ // Special handling of array property.
+ return Array.newInstance(fieldType.getComponentType(), 0);
+ }
+ else if (Collection.class.isAssignableFrom(fieldType)) {
+ return CollectionFactory.createCollection(fieldType, 0);
+ }
+ else if (Map.class.isAssignableFrom(fieldType)) {
+ return CollectionFactory.createMap(fieldType, 0);
+ }
+ } catch (IllegalArgumentException exc) {
+ return null;
+ }
}
+ // Default value: try null.
+ return null;
}
/**
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java
index 65e92007..76aacd0a 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -29,6 +29,14 @@ import org.springframework.core.annotation.AliasFor;
*
* <p>By default, all origins and headers are permitted.
*
+ * <p><b>NOTE:</b> {@code @CrossOrigin} is processed if an appropriate
+ * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
+ * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter}
+ * pair which are the default in the MVC Java config and the MVC namespace.
+ * In particular {@code @CrossOrigin} is not supported with the
+ * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter}
+ * pair both of which are also deprecated.
+ *
* @author Russell Allen
* @author Sebastien Deleuze
* @author Sam Brannen
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java
new file mode 100644
index 00000000..a437f067
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2016 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.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 DELETE} requests onto specific handler
+ * methods.
+ *
+ * <p>Specifically, {@code @DeleteMapping} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.DELETE)}.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see GetMapping
+ * @see PostMapping
+ * @see PutMapping
+ * @see PatchMapping
+ * @see RequestMapping
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@RequestMapping(method = RequestMethod.DELETE)
+public @interface DeleteMapping {
+
+ /**
+ * Alias for {@link RequestMapping#name}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String name() default "";
+
+ /**
+ * Alias for {@link RequestMapping#value}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] value() default {};
+
+ /**
+ * Alias for {@link RequestMapping#path}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] path() default {};
+
+ /**
+ * Alias for {@link RequestMapping#params}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] params() default {};
+
+ /**
+ * Alias for {@link RequestMapping#headers}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] headers() default {};
+
+ /**
+ * Alias for {@link RequestMapping#consumes}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] consumes() default {};
+
+ /**
+ * Alias for {@link RequestMapping#produces}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] produces() default {};
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java
new file mode 100644
index 00000000..12c8a6cd
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2016 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.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 GET} requests onto specific handler
+ * methods.
+ *
+ * <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}.
+ *
+ * <h5>Difference between {@code @GetMapping} &amp; {@code @RequestMapping}</h5>
+ * <p>{@code @GetMapping} does not support the {@link RequestMapping#consumes consumes}
+ * attribute of {@code @RequestMapping}.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see PostMapping
+ * @see PutMapping
+ * @see DeleteMapping
+ * @see PatchMapping
+ * @see RequestMapping
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@RequestMapping(method = RequestMethod.GET)
+public @interface GetMapping {
+
+ /**
+ * 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#produces}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] produces() default {};
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
index 39f31413..7d93e660 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
import org.springframework.ui.Model;
/**
@@ -49,6 +50,7 @@ import org.springframework.ui.Model;
* access to a {@link Model} argument.
*
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
* @since 2.5
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -57,13 +59,31 @@ import org.springframework.ui.Model;
public @interface ModelAttribute {
/**
+ * Alias for {@link #name}.
+ */
+ @AliasFor("name")
+ String value() default "";
+
+ /**
* The name of the model attribute to bind to.
* <p>The default model attribute name is inferred from the declared
* attribute type (i.e. the method parameter type or method return type),
* based on the non-qualified class name:
* e.g. "orderAddress" for class "mypackage.OrderAddress",
* or "orderAddressList" for "List&lt;mypackage.OrderAddress&gt;".
+ * @since 4.3
*/
- String value() default "";
+ @AliasFor("value")
+ String name() default "";
+
+ /**
+ * Allows declaring data binding disabled directly on an {@code @ModelAttribute}
+ * method parameter or on the attribute returned from an {@code @ModelAttribute}
+ * method, both of which would prevent data binding for that attribute.
+ * <p>By default this is set to {@code true} in which case data binding applies.
+ * Set this to {@code false} to disable data binding.
+ * @since 4.3
+ */
+ boolean binding() default true;
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java
new file mode 100644
index 00000000..82eee4ac
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2016 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.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 PATCH} requests onto specific handler
+ * methods.
+ *
+ * <p>Specifically, {@code @PatchMapping} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PATCH)}.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see GetMapping
+ * @see PostMapping
+ * @see PutMapping
+ * @see DeleteMapping
+ * @see RequestMapping
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@RequestMapping(method = RequestMethod.PATCH)
+public @interface PatchMapping {
+
+ /**
+ * Alias for {@link RequestMapping#name}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String name() default "";
+
+ /**
+ * Alias for {@link RequestMapping#value}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] value() default {};
+
+ /**
+ * Alias for {@link RequestMapping#path}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] path() default {};
+
+ /**
+ * Alias for {@link RequestMapping#params}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] params() default {};
+
+ /**
+ * Alias for {@link RequestMapping#headers}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] headers() default {};
+
+ /**
+ * Alias for {@link RequestMapping#consumes}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] consumes() default {};
+
+ /**
+ * Alias for {@link RequestMapping#produces}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] produces() default {};
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java
new file mode 100644
index 00000000..0bc64bca
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2016 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.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 POST} requests onto specific handler
+ * methods.
+ *
+ * <p>Specifically, {@code @PostMapping} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.POST)}.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see GetMapping
+ * @see PutMapping
+ * @see DeleteMapping
+ * @see PatchMapping
+ * @see RequestMapping
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@RequestMapping(method = RequestMethod.POST)
+public @interface PostMapping {
+
+ /**
+ * Alias for {@link RequestMapping#name}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String name() default "";
+
+ /**
+ * Alias for {@link RequestMapping#value}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] value() default {};
+
+ /**
+ * Alias for {@link RequestMapping#path}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] path() default {};
+
+ /**
+ * Alias for {@link RequestMapping#params}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] params() default {};
+
+ /**
+ * Alias for {@link RequestMapping#headers}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] headers() default {};
+
+ /**
+ * Alias for {@link RequestMapping#consumes}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] consumes() default {};
+
+ /**
+ * Alias for {@link RequestMapping#produces}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] produces() default {};
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java
new file mode 100644
index 00000000..8e9e71f6
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2016 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.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 PUT} requests onto specific handler
+ * methods.
+ *
+ * <p>Specifically, {@code @PutMapping} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PUT)}.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see GetMapping
+ * @see PostMapping
+ * @see DeleteMapping
+ * @see PatchMapping
+ * @see RequestMapping
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@RequestMapping(method = RequestMethod.PUT)
+public @interface PutMapping {
+
+ /**
+ * Alias for {@link RequestMapping#name}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String name() default "";
+
+ /**
+ * Alias for {@link RequestMapping#value}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] value() default {};
+
+ /**
+ * Alias for {@link RequestMapping#path}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] path() default {};
+
+ /**
+ * Alias for {@link RequestMapping#params}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] params() default {};
+
+ /**
+ * Alias for {@link RequestMapping#headers}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] headers() default {};
+
+ /**
+ * Alias for {@link RequestMapping#consumes}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] consumes() default {};
+
+ /**
+ * Alias for {@link RequestMapping#produces}.
+ */
+ @AliasFor(annotation = RequestMapping.class)
+ String[] produces() default {};
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java
new file mode 100644
index 00000000..8a51119e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2016 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.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 to bind a method parameter to a request attribute.
+ *
+ * <p>The main motivation is to provide convenient access to request attributes
+ * from a controller method with an optional/required check and a cast to the
+ * target method parameter type.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ * @see RequestMapping
+ * @see SessionAttribute
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RequestAttribute {
+
+ /**
+ * Alias for {@link #name}.
+ */
+ @AliasFor("name")
+ String value() default "";
+
+ /**
+ * The name of the request attribute to bind to.
+ * <p>The default name is inferred from the method parameter name.
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
+ * Whether the request attribute is required.
+ * <p>Defaults to {@code true}, leading to an exception being thrown if
+ * the attribute is missing. Switch this to {@code false} if you prefer
+ * a {@code null} or Java 8 {@code java.util.Optional} if the attribute
+ * doesn't exist.
+ */
+ boolean required() default true;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
index c884775c..cda331c4 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -126,6 +126,12 @@ import org.springframework.core.annotation.AliasFor;
* {@link org.springframework.validation.Errors} argument.
* Instead a {@link org.springframework.web.bind.MethodArgumentNotValidException}
* exception is raised.
+ * <li>{@link SessionAttribute @SessionAttribute} annotated parameters for access
+ * to existing, permanent session attributes (e.g. user authentication object)
+ * as opposed to model attributes temporarily stored in the session as part of
+ * a controller workflow via {@link SessionAttributes}.
+ * <li>{@link RequestAttribute @RequestAttribute} annotated parameters for access
+ * to request attributes.
* <li>{@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} parameters
* (Servlet-only) for access to the Servlet request HTTP headers and contents.
* The request stream will be converted to the entity body using
@@ -273,8 +279,16 @@ import org.springframework.core.annotation.AliasFor;
* @author Arjen Poutsma
* @author Sam Brannen
* @since 2.5
+ * @see GetMapping
+ * @see PostMapping
+ * @see PutMapping
+ * @see DeleteMapping
+ * @see PatchMapping
* @see RequestParam
+ * @see RequestAttribute
+ * @see PathVariable
* @see ModelAttribute
+ * @see SessionAttribute
* @see SessionAttributes
* @see InitBinder
* @see org.springframework.web.context.request.WebRequest
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java
index 8ad9c1e9..8843eb12 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -44,6 +44,10 @@ import org.springframework.http.HttpStatus;
* preferable to use a {@link org.springframework.http.ResponseEntity} as
* a return type and avoid the use of {@code @ResponseStatus} altogether.
*
+ * <p>Note that a controller class may also be annotated with
+ * {@code @ResponseStatus} and is then inherited by all {@code @RequestMapping}
+ * methods.
+ *
* @author Arjen Poutsma
* @author Sam Brannen
* @see org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java
index cdd3772b..2287f652 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -25,13 +25,21 @@ import java.lang.annotation.Target;
import org.springframework.stereotype.Controller;
/**
- * A convenience annotation that is itself annotated with {@link Controller @Controller}
- * and {@link ResponseBody @ResponseBody}.
+ * A convenience annotation that is itself annotated with
+ * {@link Controller @Controller} and {@link ResponseBody @ResponseBody}.
* <p>
* Types that carry this annotation are treated as controllers where
* {@link RequestMapping @RequestMapping} methods assume
* {@link ResponseBody @ResponseBody} semantics by default.
*
+ * <p><b>NOTE:</b> {@code @RestController} is processed if an appropriate
+ * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
+ * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter}
+ * pair which are the default in the MVC Java config and the MVC namespace.
+ * In particular {@code @RestController} is not supported with the
+ * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter}
+ * pair both of which are also deprecated.
+ *
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 4.0
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java
new file mode 100644
index 00000000..29b08076
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2016 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.web.bind.annotation;
+
+import java.lang.annotation.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;
+
+/**
+ * A convenience annotation that is itself annotated with
+ * {@link ControllerAdvice @ControllerAdvice}
+ * and {@link ResponseBody @ResponseBody}.
+ *
+ * <p>Types that carry this annotation are treated as controller advice where
+ * {@link ExceptionHandler @ExceptionHandler} methods assume
+ * {@link ResponseBody @ResponseBody} semantics by default.
+ *
+ * <p><b>NOTE:</b> {@code @RestControllerAdvice} is processed if an appropriate
+ * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
+ * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter} pair
+ * which are the default in the MVC Java config and the MVC namespace.
+ * In particular {@code @RestControllerAdvice} is not supported with the
+ * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter}
+ * pair both of which are also deprecated.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@ControllerAdvice
+@ResponseBody
+public @interface RestControllerAdvice {
+
+ /**
+ * Alias for the {@link #basePackages} attribute.
+ * <p>Allows for more concise annotation declarations e.g.:
+ * {@code @ControllerAdvice("org.my.pkg")} is equivalent to
+ * {@code @ControllerAdvice(basePackages="org.my.pkg")}.
+ * @see #basePackages()
+ */
+ @AliasFor("basePackages")
+ String[] value() default {};
+
+ /**
+ * Array of base packages.
+ * <p>Controllers that belong to those base packages or sub-packages thereof
+ * will be included, e.g.: {@code @ControllerAdvice(basePackages="org.my.pkg")}
+ * or {@code @ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"})}.
+ * <p>{@link #value} is an alias for this attribute, simply allowing for
+ * more concise use of the annotation.
+ * <p>Also consider using {@link #basePackageClasses()} as a type-safe
+ * alternative to String-based package names.
+ */
+ @AliasFor("value")
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #value()} for specifying the packages
+ * to select Controllers to be assisted by the {@code @ControllerAdvice}
+ * annotated class.
+ * <p>Consider creating a special no-op marker class or interface in each package
+ * that serves no purpose other than being referenced by this attribute.
+ */
+ Class<?>[] basePackageClasses() default {};
+
+ /**
+ * Array of classes.
+ * <p>Controllers that are assignable to at least one of the given types
+ * will be assisted by the {@code @ControllerAdvice} annotated class.
+ */
+ Class<?>[] assignableTypes() default {};
+
+ /**
+ * Array of annotations.
+ * <p>Controllers that are annotated with this/one of those annotation(s)
+ * will be assisted by the {@code @ControllerAdvice} annotated class.
+ * <p>Consider creating a special annotation or use a predefined one,
+ * like {@link RestController @RestController}.
+ */
+ Class<? extends Annotation>[] annotations() default {};
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java
new file mode 100644
index 00000000..d02e4c01
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2002-2016 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.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 to bind a method parameter to a session attribute.
+ *
+ * <p>The main motivation is to provide convenient access to existing, permanent
+ * session attributes (e.g. user authentication object) with an optional/required
+ * check and a cast to the target method parameter type.
+ *
+ * <p>For use cases that require adding or removing session attributes consider
+ * injecting {@code org.springframework.web.context.request.WebRequest} or
+ * {@code javax.servlet.http.HttpSession} into the controller method.
+ *
+ * <p>For temporary storage of model attributes in the session as part of the
+ * workflow for a controller, consider using {@link SessionAttributes} instead.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ * @see RequestMapping
+ * @see SessionAttributes
+ * @see RequestAttribute
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SessionAttribute {
+
+ /**
+ * Alias for {@link #name}.
+ */
+ @AliasFor("name")
+ String value() default "";
+
+ /**
+ * The name of the session attribute to bind to.
+ * <p>The default name is inferred from the method parameter name.
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
+ * Whether the session attribute is required.
+ * <p>Defaults to {@code true}, leading to an exception being thrown
+ * if the attribute is missing in the session or there is no session.
+ * Switch this to {@code false} if you prefer a {@code null} or Java 8
+ * {@code java.util.Optional} if the attribute doesn't exist.
+ */
+ boolean required() default true;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java
index 98ad9992..52850cd3 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java
@@ -26,7 +26,9 @@ import org.springframework.core.NestedRuntimeException;
* @author Juergen Hoeller
* @since 2.5.6
* @see HandlerMethodInvoker#invokeHandlerMethod
+ * @deprecated as of 4.3, in favor of the {@code HandlerMethod}-based MVC infrastructure
*/
+@Deprecated
@SuppressWarnings("serial")
public class HandlerMethodInvocationException extends NestedRuntimeException {
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java
index eb869333..9287882b 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java
@@ -94,7 +94,9 @@ import org.springframework.web.multipart.MultipartRequest;
* @author Arjen Poutsma
* @since 2.5.2
* @see #invokeHandlerMethod
+ * @deprecated as of 4.3, in favor of the {@code HandlerMethod}-based MVC infrastructure
*/
+@Deprecated
public class HandlerMethodInvoker {
private static final String MODEL_KEY_PREFIX_STALE = SessionAttributeStore.class.getName() + ".STALE.";
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java
index 9fd25c01..53d6584e 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java
@@ -48,7 +48,9 @@ import org.springframework.web.bind.annotation.SessionAttributes;
* @see org.springframework.web.bind.annotation.InitBinder
* @see org.springframework.web.bind.annotation.ModelAttribute
* @see org.springframework.web.bind.annotation.SessionAttributes
+ * @deprecated as of 4.3, in favor of the {@code HandlerMethod}-based MVC infrastructure
*/
+@Deprecated
public class HandlerMethodResolver {
private final Set<Method> handlerMethods = new LinkedHashSet<Method>();
diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
index 94b40ae9..ea3c0ae8 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -70,6 +70,9 @@ import org.springframework.web.multipart.MultipartRequest;
*/
public class WebRequestDataBinder extends WebDataBinder {
+ private static final boolean servlet3Parts = ClassUtils.hasMethod(HttpServletRequest.class, "getParts");
+
+
/**
* Create a new WebRequestDataBinder instance, with default object name.
* @param target the target object to bind onto (or {@code null}
@@ -116,7 +119,7 @@ public class WebRequestDataBinder extends WebDataBinder {
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
- else if (ClassUtils.hasMethod(HttpServletRequest.class, "getParts")) {
+ else if (servlet3Parts) {
HttpServletRequest serlvetRequest = ((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class);
new Servlet3MultipartHelper(isBindEmptyMultipartFiles()).bindParts(serlvetRequest, mpvs);
}
diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java
index eb6b5230..acbe7953 100644
--- a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -24,8 +24,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.task.AsyncListenableTaskExecutor;
@@ -41,15 +39,12 @@ import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
-import org.springframework.http.client.support.AsyncHttpAccessor;
+import org.springframework.http.client.support.InterceptingAsyncHttpAccessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
-import org.springframework.util.concurrent.FailureCallback;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureAdapter;
-import org.springframework.util.concurrent.ListenableFutureCallback;
-import org.springframework.util.concurrent.SuccessCallback;
-import org.springframework.web.util.DefaultUriTemplateHandler;
+import org.springframework.web.util.AbstractUriTemplateHandler;
import org.springframework.web.util.UriTemplateHandler;
/**
@@ -74,7 +69,7 @@ import org.springframework.web.util.UriTemplateHandler;
* @since 4.0
* @see RestTemplate
*/
-public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOperations {
+public class AsyncRestTemplate extends InterceptingAsyncHttpAccessor implements AsyncRestOperations {
private final RestTemplate syncTemplate;
@@ -157,8 +152,28 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
}
/**
- * Set a custom {@link UriTemplateHandler} for expanding URI templates.
- * <p>By default, RestTemplate uses {@link DefaultUriTemplateHandler}.
+ * Configure default URI variable values. This is a shortcut for:
+ * <pre class="code">
+ * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
+ * handler.setDefaultUriVariables(...);
+ *
+ * AsyncRestTemplate restTemplate = new AsyncRestTemplate();
+ * restTemplate.setUriTemplateHandler(handler);
+ * </pre>
+ * @param defaultUriVariables the default URI variable values
+ * @since 4.3
+ */
+ public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) {
+ UriTemplateHandler handler = this.syncTemplate.getUriTemplateHandler();
+ Assert.isInstanceOf(AbstractUriTemplateHandler.class, handler,
+ "Can only use this property in conjunction with a DefaultUriTemplateHandler");
+ ((AbstractUriTemplateHandler) handler).setDefaultUriVariables(defaultUriVariables);
+ }
+
+ /**
+ * This property has the same purpose as the corresponding property on the
+ * {@code RestTemplate}. For more details see
+ * {@link RestTemplate#setUriTemplateHandler}.
* @param handler the URI template handler to use
*/
public void setUriTemplateHandler(UriTemplateHandler handler) {
@@ -245,75 +260,37 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
// POST
@Override
- public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Object... uriVariables)
+ public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Object... uriVars)
throws RestClientException {
- AsyncRequestCallback requestCallback = httpEntityCallback(request);
- ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
- ListenableFuture<HttpHeaders> headersFuture =
- execute(url, HttpMethod.POST, requestCallback, headersExtractor, uriVariables);
- return extractLocationHeader(headersFuture);
+ AsyncRequestCallback callback = httpEntityCallback(request);
+ ResponseExtractor<HttpHeaders> extractor = headersExtractor();
+ ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor, uriVars);
+ return adaptToLocationHeader(future);
}
@Override
- public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Map<String, ?> uriVariables)
+ public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Map<String, ?> uriVars)
throws RestClientException {
- AsyncRequestCallback requestCallback = httpEntityCallback(request);
- ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
- ListenableFuture<HttpHeaders> headersFuture =
- execute(url, HttpMethod.POST, requestCallback, headersExtractor, uriVariables);
- return extractLocationHeader(headersFuture);
+ AsyncRequestCallback callback = httpEntityCallback(request);
+ ResponseExtractor<HttpHeaders> extractor = headersExtractor();
+ ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor, uriVars);
+ return adaptToLocationHeader(future);
}
@Override
public ListenableFuture<URI> postForLocation(URI url, HttpEntity<?> request) throws RestClientException {
- AsyncRequestCallback requestCallback = httpEntityCallback(request);
- ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
- ListenableFuture<HttpHeaders> headersFuture =
- execute(url, HttpMethod.POST, requestCallback, headersExtractor);
- return extractLocationHeader(headersFuture);
+ AsyncRequestCallback callback = httpEntityCallback(request);
+ ResponseExtractor<HttpHeaders> extractor = headersExtractor();
+ ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor);
+ return adaptToLocationHeader(future);
}
- private static ListenableFuture<URI> extractLocationHeader(final ListenableFuture<HttpHeaders> headersFuture) {
- return new ListenableFuture<URI>() {
- @Override
- public void addCallback(final ListenableFutureCallback<? super URI> callback) {
- addCallback(callback, callback);
- }
- @Override
- public void addCallback(final SuccessCallback<? super URI> successCallback, final FailureCallback failureCallback) {
- headersFuture.addCallback(new ListenableFutureCallback<HttpHeaders>() {
- @Override
- public void onSuccess(HttpHeaders result) {
- successCallback.onSuccess(result.getLocation());
- }
- @Override
- public void onFailure(Throwable ex) {
- failureCallback.onFailure(ex);
- }
- });
- }
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- return headersFuture.cancel(mayInterruptIfRunning);
- }
- @Override
- public boolean isCancelled() {
- return headersFuture.isCancelled();
- }
- @Override
- public boolean isDone() {
- return headersFuture.isDone();
- }
+ private static ListenableFuture<URI> adaptToLocationHeader(ListenableFuture<HttpHeaders> future) {
+ return new ListenableFutureAdapter<URI, HttpHeaders>(future) {
@Override
- public URI get() throws InterruptedException, ExecutionException {
- HttpHeaders headers = headersFuture.get();
- return headers.getLocation();
- }
- @Override
- public URI get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
- HttpHeaders headers = headersFuture.get(timeout, unit);
+ protected URI adapt(HttpHeaders headers) throws ExecutionException {
return headers.getLocation();
}
};
@@ -389,71 +366,35 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
// OPTIONS
@Override
- public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Object... uriVariables) throws RestClientException {
- ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
- ListenableFuture<HttpHeaders> headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor, uriVariables);
- return extractAllowHeader(headersFuture);
+ public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Object... uriVars) throws RestClientException {
+ ResponseExtractor<HttpHeaders> extractor = headersExtractor();
+ ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor, uriVars);
+ return adaptToAllowHeader(future);
}
@Override
- public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Map<String, ?> uriVariables) throws RestClientException {
- ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
- ListenableFuture<HttpHeaders> headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor, uriVariables);
- return extractAllowHeader(headersFuture);
+ public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Map<String, ?> uriVars) throws RestClientException {
+ ResponseExtractor<HttpHeaders> extractor = headersExtractor();
+ ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor, uriVars);
+ return adaptToAllowHeader(future);
}
@Override
public ListenableFuture<Set<HttpMethod>> optionsForAllow(URI url) throws RestClientException {
- ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
- ListenableFuture<HttpHeaders> headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor);
- return extractAllowHeader(headersFuture);
+ ResponseExtractor<HttpHeaders> extractor = headersExtractor();
+ ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor);
+ return adaptToAllowHeader(future);
}
- private static ListenableFuture<Set<HttpMethod>> extractAllowHeader(final ListenableFuture<HttpHeaders> headersFuture) {
- return new ListenableFuture<Set<HttpMethod>>() {
+ private static ListenableFuture<Set<HttpMethod>> adaptToAllowHeader(ListenableFuture<HttpHeaders> future) {
+ return new ListenableFutureAdapter<Set<HttpMethod>, HttpHeaders>(future) {
@Override
- public void addCallback(final ListenableFutureCallback<? super Set<HttpMethod>> callback) {
- addCallback(callback, callback);
- }
- @Override
- public void addCallback(final SuccessCallback<? super Set<HttpMethod>> successCallback, final FailureCallback failureCallback) {
- headersFuture.addCallback(new ListenableFutureCallback<HttpHeaders>() {
- @Override
- public void onSuccess(HttpHeaders result) {
- successCallback.onSuccess(result.getAllow());
- }
- @Override
- public void onFailure(Throwable ex) {
- failureCallback.onFailure(ex);
- }
- });
- }
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- return headersFuture.cancel(mayInterruptIfRunning);
- }
- @Override
- public boolean isCancelled() {
- return headersFuture.isCancelled();
- }
- @Override
- public boolean isDone() {
- return headersFuture.isDone();
- }
- @Override
- public Set<HttpMethod> get() throws InterruptedException, ExecutionException {
- HttpHeaders headers = headersFuture.get();
- return headers.getAllow();
- }
- @Override
- public Set<HttpMethod> get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
- HttpHeaders headers = headersFuture.get(timeout, unit);
+ protected Set<HttpMethod> adapt(HttpHeaders headers) throws ExecutionException {
return headers.getAllow();
}
};
}
-
// exchange
@Override
diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java
index 45eb677e..591b95e6 100644
--- a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java
@@ -113,7 +113,7 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler {
private Charset getCharset(ClientHttpResponse response) {
HttpHeaders headers = response.getHeaders();
MediaType contentType = headers.getContentType();
- return contentType != null ? contentType.getCharSet() : null;
+ return contentType != null ? contentType.getCharset() : null;
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java
index 7eada90c..2f03908c 100644
--- a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java
+++ b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java
@@ -76,7 +76,7 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
@Override
- @SuppressWarnings({"unchecked", "rawtypes"})
+ @SuppressWarnings({"unchecked", "rawtypes", "resource"})
public T extractData(ClientHttpResponse response) throws IOException {
MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java b/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java
index 93b3c110..0d5c2fbc 100644
--- a/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java
+++ b/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,7 +16,6 @@
package org.springframework.web.client;
-import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import org.springframework.http.HttpHeaders;
@@ -27,29 +26,19 @@ import org.springframework.http.HttpStatus;
*
* @author Arjen Poutsma
* @author Chris Beams
+ * @author Rossen Stoyanchev
* @since 3.0
*/
-public abstract class HttpStatusCodeException extends RestClientException {
+public abstract class HttpStatusCodeException extends RestClientResponseException {
- private static final long serialVersionUID = -5807494703720513267L;
-
- private static final String DEFAULT_CHARSET = "ISO-8859-1";
+ private static final long serialVersionUID = 5696801857651587810L;
private final HttpStatus statusCode;
- private final String statusText;
-
- private final byte[] responseBody;
-
- private final HttpHeaders responseHeaders;
-
- private final String responseCharset;
-
/**
- * Construct a new instance of {@code HttpStatusCodeException} based on an
- * {@link HttpStatus}.
+ * Construct a new instance with an {@link HttpStatus}.
* @param statusCode the status code
*/
protected HttpStatusCodeException(HttpStatus statusCode) {
@@ -57,8 +46,7 @@ public abstract class HttpStatusCodeException extends RestClientException {
}
/**
- * Construct a new instance of {@code HttpStatusCodeException} based on an
- * {@link HttpStatus} and status text.
+ * Construct a new instance with an {@link HttpStatus} and status text.
* @param statusCode the status code
* @param statusText the status text
*/
@@ -67,23 +55,22 @@ public abstract class HttpStatusCodeException extends RestClientException {
}
/**
- * Construct a new instance of {@code HttpStatusCodeException} based on an
- * {@link HttpStatus}, status text, and response body content.
+ * Construct instance with an {@link HttpStatus}, status text, and content.
* @param statusCode the status code
* @param statusText the status text
* @param responseBody the response body content, may be {@code null}
* @param responseCharset the response body charset, may be {@code null}
* @since 3.0.5
*/
- protected HttpStatusCodeException(
- HttpStatus statusCode, String statusText, byte[] responseBody, Charset responseCharset) {
+ protected HttpStatusCodeException(HttpStatus statusCode, String statusText,
+ byte[] responseBody, Charset responseCharset) {
this(statusCode, statusText, null, responseBody, responseCharset);
}
/**
- * Construct a new instance of {@code HttpStatusCodeException} based on an
- * {@link HttpStatus}, status text, and response body content.
+ * Construct instance with an {@link HttpStatus}, status text, content, and
+ * a response charset.
* @param statusCode the status code
* @param statusText the status text
* @param responseHeaders the response headers, may be {@code null}
@@ -94,12 +81,9 @@ public abstract class HttpStatusCodeException extends RestClientException {
protected HttpStatusCodeException(HttpStatus statusCode, String statusText,
HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset) {
- super(statusCode.value() + " " + statusText);
+ super(statusCode.value() + " " + statusText, statusCode.value(), statusText,
+ responseHeaders, responseBody, responseCharset);
this.statusCode = statusCode;
- this.statusText = statusText;
- this.responseHeaders = responseHeaders;
- this.responseBody = responseBody != null ? responseBody : new byte[0];
- this.responseCharset = responseCharset != null ? responseCharset.name() : DEFAULT_CHARSET;
}
@@ -110,41 +94,4 @@ public abstract class HttpStatusCodeException extends RestClientException {
return this.statusCode;
}
- /**
- * Return the HTTP status text.
- */
- public String getStatusText() {
- return this.statusText;
- }
-
- /**
- * Return the HTTP response headers.
- * @since 3.1.2
- */
- public HttpHeaders getResponseHeaders() {
- return this.responseHeaders;
- }
-
- /**
- * Return the response body as a byte array.
- * @since 3.0.5
- */
- public byte[] getResponseBodyAsByteArray() {
- return this.responseBody;
- }
-
- /**
- * Return the response body as a string.
- * @since 3.0.5
- */
- public String getResponseBodyAsString() {
- try {
- return new String(this.responseBody, this.responseCharset);
- }
- catch (UnsupportedEncodingException ex) {
- // should not occur
- throw new IllegalStateException(ex);
- }
- }
-
}
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java
new file mode 100644
index 00000000..d9c1e6a0
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2016 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.web.client;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+import org.springframework.http.HttpHeaders;
+
+/**
+ * Common base class for exceptions that contain actual HTTP response data.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+public class RestClientResponseException extends RestClientException {
+
+ private static final long serialVersionUID = -8803556342728481792L;
+
+ private static final String DEFAULT_CHARSET = "ISO-8859-1";
+
+
+ private final int rawStatusCode;
+
+ private final String statusText;
+
+ private final byte[] responseBody;
+
+ private final HttpHeaders responseHeaders;
+
+ private final String responseCharset;
+
+
+ /**
+ * Construct a new instance of with the given response data.
+ * @param statusCode the raw status code value
+ * @param statusText the status text
+ * @param responseHeaders the response headers (may be {@code null})
+ * @param responseBody the response body content (may be {@code null})
+ * @param responseCharset the response body charset (may be {@code null})
+ */
+ public RestClientResponseException(String message, int statusCode, String statusText,
+ HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset) {
+
+ super(message);
+ this.rawStatusCode = statusCode;
+ this.statusText = statusText;
+ this.responseHeaders = responseHeaders;
+ this.responseBody = (responseBody != null ? responseBody : new byte[0]);
+ this.responseCharset = (responseCharset != null ? responseCharset.name() : DEFAULT_CHARSET);
+ }
+
+
+ /**
+ * Return the raw HTTP status code value.
+ */
+ public int getRawStatusCode() {
+ return this.rawStatusCode;
+ }
+
+ /**
+ * Return the HTTP status text.
+ */
+ public String getStatusText() {
+ return this.statusText;
+ }
+
+ /**
+ * Return the HTTP response headers.
+ */
+ public HttpHeaders getResponseHeaders() {
+ return this.responseHeaders;
+ }
+
+ /**
+ * Return the response body as a byte array.
+ */
+ public byte[] getResponseBodyAsByteArray() {
+ return this.responseBody;
+ }
+
+ /**
+ * Return the response body as a string.
+ */
+ public String getResponseBodyAsString() {
+ try {
+ return new String(this.responseBody, this.responseCharset);
+ }
+ catch (UnsupportedEncodingException ex) {
+ // should not occur
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
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 8c7c0433..8a7f79a2 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
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -51,6 +51,7 @@ import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConve
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.web.util.AbstractUriTemplateHandler;
import org.springframework.web.util.DefaultUriTemplateHandler;
import org.springframework.web.util.UriTemplateHandler;
@@ -237,8 +238,30 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
/**
- * Set a custom {@link UriTemplateHandler} for expanding URI templates.
- * <p>By default, RestTemplate uses {@link DefaultUriTemplateHandler}.
+ * Configure default URI variable values. This is a shortcut for:
+ * <pre class="code">
+ * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
+ * handler.setDefaultUriVariables(...);
+ *
+ * RestTemplate restTemplate = new RestTemplate();
+ * restTemplate.setUriTemplateHandler(handler);
+ * </pre>
+ * @param defaultUriVariables the default URI variable values
+ * @since 4.3
+ */
+ public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) {
+ Assert.isInstanceOf(AbstractUriTemplateHandler.class, this.uriTemplateHandler,
+ "Can only use this property in conjunction with an AbstractUriTemplateHandler");
+ ((AbstractUriTemplateHandler) this.uriTemplateHandler).setDefaultUriVariables(defaultUriVariables);
+ }
+
+ /**
+ * Configure the {@link UriTemplateHandler} to use to expand URI templates.
+ * By default the {@link DefaultUriTemplateHandler} is used which relies on
+ * Spring's URI template support and exposes several useful properties that
+ * customize its behavior for encoding and for prepending a common base URL.
+ * An alternative implementation may be used to plug an external URI
+ * template library.
* @param handler the URI template handler to use
*/
public void setUriTemplateHandler(UriTemplateHandler handler) {
@@ -603,8 +626,11 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
}
catch (IOException ex) {
+ String resource = url.toString();
+ String query = url.getRawQuery();
+ resource = (query != null ? resource.substring(0, resource.indexOf(query) - 1) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
- " request for \"" + url + "\": " + ex.getMessage(), ex);
+ " request for \"" + resource + "\": " + ex.getMessage(), ex);
}
finally {
if (response != null) {
@@ -728,7 +754,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
List<MediaType> supportedMediaTypes = messageConverter.getSupportedMediaTypes();
List<MediaType> result = new ArrayList<MediaType>(supportedMediaTypes.size());
for (MediaType supportedMediaType : supportedMediaTypes) {
- if (supportedMediaType.getCharSet() != null) {
+ if (supportedMediaType.getCharset() != null) {
supportedMediaType =
new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
}
@@ -779,11 +805,34 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
else {
Object requestBody = this.requestEntity.getBody();
- Class<?> requestType = requestBody.getClass();
+ Class<?> requestBodyClass = requestBody.getClass();
+ Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
+ ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
HttpHeaders requestHeaders = this.requestEntity.getHeaders();
MediaType requestContentType = requestHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
- if (messageConverter.canWrite(requestType, requestContentType)) {
+ if (messageConverter instanceof GenericHttpMessageConverter) {
+ GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
+ if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
+ if (!requestHeaders.isEmpty()) {
+ httpRequest.getHeaders().putAll(requestHeaders);
+ }
+ if (logger.isDebugEnabled()) {
+ if (requestContentType != null) {
+ logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
+ "\" using [" + messageConverter + "]");
+ }
+ else {
+ logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
+ }
+
+ }
+ genericMessageConverter.write(
+ requestBody, requestBodyType, requestContentType, httpRequest);
+ return;
+ }
+ }
+ else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
if (!requestHeaders.isEmpty()) {
httpRequest.getHeaders().putAll(requestHeaders);
}
@@ -803,7 +852,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
}
String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
- requestType.getName() + "]";
+ requestBodyClass.getName() + "]";
if (requestContentType != null) {
message += " and content type [" + requestContentType + "]";
}
diff --git a/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java b/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java
index 40ac5047..b8e894b9 100644
--- a/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java
+++ b/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,7 +16,6 @@
package org.springframework.web.client;
-import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import org.springframework.http.HttpHeaders;
@@ -28,21 +27,9 @@ import org.springframework.http.HttpStatus;
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class UnknownHttpStatusCodeException extends RestClientException {
+public class UnknownHttpStatusCodeException extends RestClientResponseException {
- private static final long serialVersionUID = 4702443689088991600L;
-
- private static final String DEFAULT_CHARSET = "ISO-8859-1";
-
- private final int rawStatusCode;
-
- private final String statusText;
-
- private final byte[] responseBody;
-
- private final HttpHeaders responseHeaders;
-
- private final String responseCharset;
+ private static final long serialVersionUID = 7103980251635005491L;
/**
@@ -57,54 +44,8 @@ public class UnknownHttpStatusCodeException extends RestClientException {
public UnknownHttpStatusCodeException(int rawStatusCode, String statusText,
HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset) {
- super("Unknown status code [" + String.valueOf(rawStatusCode) + "]" + " " + statusText);
- this.rawStatusCode = rawStatusCode;
- this.statusText = statusText;
- this.responseHeaders = responseHeaders;
- this.responseBody = responseBody != null ? responseBody : new byte[0];
- this.responseCharset = responseCharset != null ? responseCharset.name() : DEFAULT_CHARSET;
- }
-
-
- /**
- * Return the raw HTTP status code value.
- */
- public int getRawStatusCode() {
- return this.rawStatusCode;
- }
-
- /**
- * Return the HTTP status text.
- */
- public String getStatusText() {
- return this.statusText;
- }
-
- /**
- * Return the HTTP response headers.
- */
- public HttpHeaders getResponseHeaders() {
- return this.responseHeaders;
- }
-
- /**
- * Return the response body as a byte array.
- */
- public byte[] getResponseBodyAsByteArray() {
- return this.responseBody;
- }
-
- /**
- * Return the response body as a string.
- */
- public String getResponseBodyAsString() {
- try {
- return new String(this.responseBody, this.responseCharset);
- }
- catch (UnsupportedEncodingException ex) {
- // should not occur
- throw new IllegalStateException(ex);
- }
+ super("Unknown status code [" + String.valueOf(rawStatusCode) + "]" + " " + statusText,
+ rawStatusCode, statusText, responseHeaders, responseBody, responseCharset);
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java b/spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java
new file mode 100644
index 00000000..fa7e897c
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2016 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.web.context.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.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.web.context.WebApplicationContext;
+
+/**
+ * {@code @ApplicationScope} is a specialization of {@link Scope @Scope} for a
+ * component whose lifecycle is bound to the current web application.
+ *
+ * <p>Specifically, {@code @ApplicationScope} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @Scope("application")} with the default
+ * {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}.
+ *
+ * <p>{@code @ApplicationScope} may be used as a meta-annotation to create custom
+ * composed annotations.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see RequestScope
+ * @see SessionScope
+ * @see org.springframework.context.annotation.Scope
+ * @see org.springframework.web.context.WebApplicationContext#SCOPE_APPLICATION
+ * @see org.springframework.web.context.support.ServletContextScope
+ * @see org.springframework.stereotype.Component
+ * @see org.springframework.context.annotation.Bean
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Scope(WebApplicationContext.SCOPE_APPLICATION)
+public @interface ApplicationScope {
+
+ /**
+ * Alias for {@link Scope#proxyMode}.
+ * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
+ */
+ @AliasFor(annotation = Scope.class)
+ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java b/spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java
new file mode 100644
index 00000000..f33a1dd7
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2016 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.web.context.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.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.web.context.WebApplicationContext;
+
+/**
+ * {@code @RequestScope} is a specialization of {@link Scope @Scope} for a
+ * component whose lifecycle is bound to the current web request.
+ *
+ * <p>Specifically, {@code @RequestScope} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @Scope("request")} with the default
+ * {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}.
+ *
+ * <p>{@code @RequestScope} may be used as a meta-annotation to create custom
+ * composed annotations.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see SessionScope
+ * @see ApplicationScope
+ * @see org.springframework.context.annotation.Scope
+ * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
+ * @see org.springframework.web.context.request.RequestScope
+ * @see org.springframework.stereotype.Component
+ * @see org.springframework.context.annotation.Bean
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Scope(WebApplicationContext.SCOPE_REQUEST)
+public @interface RequestScope {
+
+ /**
+ * Alias for {@link Scope#proxyMode}.
+ * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
+ */
+ @AliasFor(annotation = Scope.class)
+ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java b/spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java
new file mode 100644
index 00000000..45af3c6e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2016 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.web.context.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.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.web.context.WebApplicationContext;
+
+/**
+ * {@code @SessionScope} is a specialization of {@link Scope @Scope} for a
+ * component whose lifecycle is bound to the current web session.
+ *
+ * <p>Specifically, {@code @SessionScope} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @Scope("session")} with the default
+ * {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}.
+ *
+ * <p>{@code @SessionScope} may be used as a meta-annotation to create custom
+ * composed annotations.
+ *
+ * @author Sam Brannen
+ * @since 4.3
+ * @see RequestScope
+ * @see ApplicationScope
+ * @see org.springframework.context.annotation.Scope
+ * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
+ * @see org.springframework.web.context.request.SessionScope
+ * @see org.springframework.stereotype.Component
+ * @see org.springframework.context.annotation.Bean
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Scope(WebApplicationContext.SCOPE_SESSION)
+public @interface SessionScope {
+
+ /**
+ * Alias for {@link Scope#proxyMode}.
+ * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
+ */
+ @AliasFor(annotation = Scope.class)
+ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java b/spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java
new file mode 100644
index 00000000..fffce1b0
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Provides convenience annotations for web scopes.
+ */
+package org.springframework.web.context.annotation;
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java b/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java
index 73301c34..bfbf032b 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -217,7 +217,7 @@ public class FacesRequestAttributes implements RequestAttributes {
Object session = getExternalContext().getSession(true);
try {
// Both HttpSession and PortletSession have a getId() method.
- Method getIdMethod = session.getClass().getMethod("getId", new Class<?>[0]);
+ Method getIdMethod = session.getClass().getMethod("getId");
return ReflectionUtils.invokeMethod(getIdMethod, session).toString();
}
catch (NoSuchMethodException ex) {
@@ -227,12 +227,12 @@ public class FacesRequestAttributes implements RequestAttributes {
@Override
public Object getSessionMutex() {
- // Enforce presence of a session first to allow listeners
- // to create the mutex attribute, if any.
- Object session = getExternalContext().getSession(true);
- Object mutex = getExternalContext().getSessionMap().get(WebUtils.SESSION_MUTEX_ATTRIBUTE);
+ // Enforce presence of a session first to allow listeners to create the mutex attribute
+ ExternalContext externalContext = getExternalContext();
+ Object session = externalContext.getSession(true);
+ Object mutex = externalContext.getSessionMap().get(WebUtils.SESSION_MUTEX_ATTRIBUTE);
if (mutex == null) {
- mutex = session;
+ mutex = (session != null ? session : externalContext);
}
return mutex;
}
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java
index 4182342a..6eb5de18 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -108,15 +108,24 @@ public class ServletRequestAttributes extends AbstractRequestAttributes {
*/
protected final HttpSession getSession(boolean allowCreate) {
if (isRequestActive()) {
- return this.request.getSession(allowCreate);
+ HttpSession session = this.request.getSession(allowCreate);
+ this.session = session;
+ return session;
}
else {
// Access through stored session reference, if any...
- if (this.session == null && allowCreate) {
- throw new IllegalStateException(
- "No session found and request already completed - cannot create new session!");
+ HttpSession session = this.session;
+ if (session == null) {
+ if (allowCreate) {
+ throw new IllegalStateException(
+ "No session found and request already completed - cannot create new session!");
+ }
+ else {
+ session = this.request.getSession(false);
+ this.session = session;
+ }
}
- return this.session;
+ return session;
}
}
@@ -251,25 +260,26 @@ public class ServletRequestAttributes extends AbstractRequestAttributes {
*/
@Override
protected void updateAccessedSessionAttributes() {
- // Store session reference for access after request completion.
- this.session = this.request.getSession(false);
- // Update all affected session attributes.
- if (this.session != null) {
- try {
- for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) {
- String name = entry.getKey();
- Object newValue = entry.getValue();
- Object oldValue = this.session.getAttribute(name);
- if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) {
- this.session.setAttribute(name, newValue);
+ if (!this.sessionAttributesToUpdate.isEmpty()) {
+ // Update all affected session attributes.
+ HttpSession session = getSession(false);
+ if (session != null) {
+ try {
+ for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) {
+ String name = entry.getKey();
+ Object newValue = entry.getValue();
+ Object oldValue = session.getAttribute(name);
+ if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) {
+ session.setAttribute(name, newValue);
+ }
}
}
+ catch (IllegalStateException ex) {
+ // Session invalidated - shouldn't usually happen.
+ }
}
- catch (IllegalStateException ex) {
- // Session invalidated - shouldn't usually happen.
- }
+ this.sessionAttributesToUpdate.clear();
}
- this.sessionAttributesToUpdate.clear();
}
/**
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
index 6cc8e052..97a227af 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -21,6 +21,8 @@ import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@@ -47,6 +49,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
+ private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
+
private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
private static final String HEADER_LAST_MODIFIED = "Last-Modified";
@@ -55,6 +59,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
private static final String METHOD_HEAD = "HEAD";
+ private static final String METHOD_POST = "POST";
+
+ private static final String METHOD_PUT = "PUT";
+
+ private static final String METHOD_DELETE = "DELETE";
+
+ /**
+ * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match"
+ * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
+ */
+ private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
+
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
private static final boolean servlet3Present =
@@ -183,11 +199,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
if (isCompatibleWithConditionalRequests(response)) {
this.notModified = isTimestampNotModified(lastModifiedTimestamp);
if (response != null) {
- if (this.notModified && supportsNotModifiedStatus()) {
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ if (supportsNotModifiedStatus()) {
+ if (this.notModified) {
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ }
+ if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
+ response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
+ }
}
- if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
- response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
+ else if (supportsConditionalUpdate()) {
+ if (this.notModified) {
+ response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
+ }
}
}
}
@@ -201,7 +224,9 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
if (StringUtils.hasLength(etag) && !this.notModified) {
if (isCompatibleWithConditionalRequests(response)) {
etag = addEtagPadding(etag);
- this.notModified = isEtagNotModified(etag);
+ if (hasRequestHeader(HEADER_IF_NONE_MATCH)) {
+ this.notModified = isEtagNotModified(etag);
+ }
if (response != null) {
if (this.notModified && supportsNotModifiedStatus()) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
@@ -221,16 +246,28 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
if (StringUtils.hasLength(etag) && !this.notModified) {
if (isCompatibleWithConditionalRequests(response)) {
etag = addEtagPadding(etag);
- this.notModified = isEtagNotModified(etag) && isTimestampNotModified(lastModifiedTimestamp);
+ if (hasRequestHeader(HEADER_IF_NONE_MATCH)) {
+ this.notModified = isEtagNotModified(etag);
+ }
+ else if (hasRequestHeader(HEADER_IF_MODIFIED_SINCE)) {
+ this.notModified = isTimestampNotModified(lastModifiedTimestamp);
+ }
if (response != null) {
- if (this.notModified && supportsNotModifiedStatus()) {
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
- }
- if (isHeaderAbsent(response, HEADER_ETAG)) {
- response.setHeader(HEADER_ETAG, etag);
+ if (supportsNotModifiedStatus()) {
+ if (this.notModified) {
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ }
+ if (isHeaderAbsent(response, HEADER_ETAG)) {
+ response.setHeader(HEADER_ETAG, etag);
+ }
+ if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
+ response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
+ }
}
- if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
- response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
+ else if (supportsConditionalUpdate()) {
+ if (this.notModified) {
+ response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
+ }
}
}
}
@@ -250,7 +287,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return true;
}
return HttpStatus.valueOf(response.getStatus()).is2xxSuccessful();
- } catch (IllegalArgumentException e) {
+ }
+ catch (IllegalArgumentException e) {
return true;
}
}
@@ -263,47 +301,65 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return (response.getHeader(header) == null);
}
+ private boolean hasRequestHeader(String headerName) {
+ return StringUtils.hasLength(getHeader(headerName));
+ }
+
private boolean supportsNotModifiedStatus() {
String method = getRequest().getMethod();
return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method));
}
- @SuppressWarnings("deprecation")
+ private boolean supportsConditionalUpdate() {
+ String method = getRequest().getMethod();
+ return (METHOD_POST.equals(method) || METHOD_PUT.equals(method) || METHOD_DELETE.equals(method))
+ && hasRequestHeader(HEADER_IF_UNMODIFIED_SINCE);
+ }
+
private boolean isTimestampNotModified(long lastModifiedTimestamp) {
- long ifModifiedSince = -1;
+ long ifModifiedSince = parseDateHeader(HEADER_IF_MODIFIED_SINCE);
+ if (ifModifiedSince != -1) {
+ return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
+ }
+ long ifUnmodifiedSince = parseDateHeader(HEADER_IF_UNMODIFIED_SINCE);
+ if (ifUnmodifiedSince != -1) {
+ return (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
+ }
+ return false;
+ }
+
+ @SuppressWarnings("deprecation")
+ private long parseDateHeader(String headerName) {
+ long dateValue = -1;
try {
- ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE);
+ dateValue = getRequest().getDateHeader(headerName);
}
catch (IllegalArgumentException ex) {
- String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE);
+ String headerValue = getHeader(headerName);
// Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
int separatorIndex = headerValue.indexOf(';');
if (separatorIndex != -1) {
String datePart = headerValue.substring(0, separatorIndex);
try {
- ifModifiedSince = Date.parse(datePart);
+ dateValue = Date.parse(datePart);
}
catch (IllegalArgumentException ex2) {
// Giving up
}
}
}
- return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
+ return dateValue;
}
private boolean isEtagNotModified(String etag) {
- if (StringUtils.hasLength(etag)) {
- String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH);
- if (StringUtils.hasLength(ifNoneMatch)) {
- String[] clientEtags = StringUtils.delimitedListToStringArray(ifNoneMatch, ",", " ");
- for (String clientEtag : clientEtags) {
- // compare weak/strong ETag as per https://tools.ietf.org/html/rfc7232#section-2.3
- if (StringUtils.hasLength(clientEtag) &&
- (clientEtag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", "")) ||
- clientEtag.equals("*"))) {
- return true;
- }
- }
+ String ifNoneMatch = getHeader(HEADER_IF_NONE_MATCH);
+ // compare weak/strong ETag as per https://tools.ietf.org/html/rfc7232#section-2.3
+ String serverETag = etag.replaceFirst("^W/", "");
+ Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(ifNoneMatch);
+ while (eTagMatcher.find()) {
+ if ("*".equals(eTagMatcher.group())
+ || serverETag.equals(eTagMatcher.group(3))) {
+ return true;
}
}
return false;
@@ -316,7 +372,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return etag;
}
-
@Override
public String getDescription(boolean includeClientInfo) {
HttpServletRequest request = getRequest();
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java
index cea9fa70..c4c11eb4 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -126,10 +126,10 @@ public interface WebRequest extends RequestAttributes {
boolean isSecure();
/**
- * Check whether the request qualifies as not modified given the
+ * Check whether the requested resource has been modified given the
* supplied last-modified timestamp (as determined by the application).
- * <p>This will also transparently set the appropriate response headers,
- * for both the modified case and the not-modified case.
+ * <p>This will also transparently set the "Last-Modified" response header
+ * and HTTP status when applicable.
* <p>Typical usage:
* <pre class="code">
* public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -142,6 +142,8 @@ public interface WebRequest extends RequestAttributes {
* model.addAttribute(...);
* return "myViewName";
* }</pre>
+ * <p>This method works with conditional GET/HEAD requests, but
+ * also with conditional POST/PUT/DELETE requests.
* <p><strong>Note:</strong> you can use either
* this {@code #checkNotModified(long)} method; or
* {@link #checkNotModified(String)}. If you want enforce both
@@ -151,8 +153,9 @@ public interface WebRequest extends RequestAttributes {
* <p>If the "If-Modified-Since" header is set but cannot be parsed
* to a date value, this method will ignore the header and proceed
* with setting the last-modified timestamp on the response.
- * @param lastModifiedTimestamp the last-modified timestamp that
- * the application determined for the underlying resource
+ * @param lastModifiedTimestamp the last-modified timestamp in
+ * milliseconds that the application determined for the underlying
+ * resource
* @return whether the request qualifies as not modified,
* allowing to abort request processing and relying on the response
* telling the client that the content has not been modified
@@ -160,10 +163,10 @@ public interface WebRequest extends RequestAttributes {
boolean checkNotModified(long lastModifiedTimestamp);
/**
- * Check whether the request qualifies as not modified given the
+ * Check whether the requested resource has been modified given the
* supplied {@code ETag} (entity tag), as determined by the application.
- * <p>This will also transparently set the appropriate response headers,
- * for both the modified case and the not-modified case.
+ * <p>This will also transparently set the "ETag" response header
+ * and HTTP status when applicable.
* <p>Typical usage:
* <pre class="code">
* public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -185,18 +188,16 @@ public interface WebRequest extends RequestAttributes {
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
- * @return whether the request qualifies as not modified,
- * allowing to abort request processing and relying on the response
- * telling the client that the content has not been modified
+ * @return true if the request does not require further processing.
*/
boolean checkNotModified(String etag);
/**
- * Check whether the request qualifies as not modified given the
+ * Check whether the requested resource has been modified given the
* supplied {@code ETag} (entity tag) and last-modified timestamp,
* as determined by the application.
* <p>This will also transparently set the "ETag" and "Last-Modified"
- * response headers, for both the modified case and the not-modified case.
+ * response headers, and HTTP status when applicable.
* <p>Typical usage:
* <pre class="code">
* public String myHandleMethod(WebRequest webRequest, Model model) {
@@ -210,6 +211,8 @@ public interface WebRequest extends RequestAttributes {
* model.addAttribute(...);
* return "myViewName";
* }</pre>
+ * <p>This method works with conditional GET/HEAD requests, but
+ * also with conditional POST/PUT/DELETE requests.
* <p><strong>Note:</strong> The HTTP specification recommends
* setting both ETag and Last-Modified values, but you can also
* use {@code #checkNotModified(String)} or
@@ -217,11 +220,10 @@ public interface WebRequest extends RequestAttributes {
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
- * @param lastModifiedTimestamp the last-modified timestamp that
- * the application determined for the underlying resource
- * @return whether the request qualifies as not modified,
- * allowing to abort request processing and relying on the response
- * telling the client that the content has not been modified
+ * @param lastModifiedTimestamp the last-modified timestamp in
+ * milliseconds that the application determined for the underlying
+ * resource
+ * @return true if the request does not require further processing.
* @since 4.2
*/
boolean checkNotModified(String etag, long lastModifiedTimestamp);
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java
index 638068a3..89e5745f 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java
@@ -25,7 +25,7 @@ import org.springframework.web.context.request.NativeWebRequest;
*
* <p>A {@code DeferredResultProcessingInterceptor} is invoked before the start
* of async processing, after the {@code DeferredResult} is set as well as on
- * timeout, or or after completing for any reason including a timeout or network
+ * timeout, or after completing for any reason including a timeout or network
* error.
*
* <p>As a general rule exceptions raised by interceptor methods will cause
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
index fc0bb983..9bd4ac5c 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
@@ -34,7 +34,7 @@ import org.springframework.web.context.request.ServletWebRequest;
*
* <p>The servlet and all filters involved in an async request must have async
* support enabled using the Servlet API or by adding an
- * {@code <async-support>true</async-support>} element to servlet and filter
+ * {@code <async-supported>true</async-supported>} element to servlet and filter
* declarations in {@code web.xml}.
*
* @author Rossen Stoyanchev
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
index a20cf23d..83913f02 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -105,7 +105,6 @@ public final class WebAsyncManager {
*/
public void setAsyncWebRequest(final AsyncWebRequest asyncWebRequest) {
Assert.notNull(asyncWebRequest, "AsyncWebRequest must not be null");
- Assert.state(!isConcurrentHandlingStarted(), "Can't set AsyncWebRequest with concurrent handling in progress");
this.asyncWebRequest = asyncWebRequest;
this.asyncWebRequest.addCompletionHandler(new Runnable() {
@Override
diff --git a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java
index a05b782f..a13890b0 100644
--- a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java
+++ b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -77,6 +77,7 @@ public class ServletContextResource extends AbstractFileResolvingResource implem
this.path = pathToUse;
}
+
/**
* Return the ServletContext for this resource.
*/
@@ -91,7 +92,6 @@ public class ServletContextResource extends AbstractFileResolvingResource implem
return this.path;
}
-
/**
* This implementation checks {@code ServletContext.getResource}.
* @see javax.servlet.ServletContext#getResource(String)
diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
index 76daa33b..016a355b 100644
--- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -340,6 +340,7 @@ public class CorsConfiguration {
}
if (allowedMethods.isEmpty()) {
allowedMethods.add(HttpMethod.GET.name());
+ allowedMethods.add(HttpMethod.HEAD.name());
}
List<HttpMethod> result = new ArrayList<HttpMethod>(allowedMethods.size());
boolean allowed = false;
diff --git a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java
index 4062bf6d..6a9af746 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java
@@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
@@ -72,6 +73,8 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
private boolean includeClientInfo = false;
+ private boolean includeHeaders = false;
+
private boolean includePayload = false;
private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
@@ -120,6 +123,24 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
}
/**
+ * Set whether the request headers should be included in the log message.
+ * <p>Should be configured using an {@code <init-param>} for parameter name
+ * "includeHeaders" in the filter definition in {@code web.xml}.
+ * @since 4.3
+ */
+ public void setIncludeHeaders(boolean includeHeaders) {
+ this.includeHeaders = includeHeaders;
+ }
+
+ /**
+ * Return whether the request headers should be included in the log message.
+ * @since 4.3
+ */
+ public boolean isIncludeHeaders() {
+ return this.includeHeaders;
+ }
+
+ /**
* Set whether the request payload (body) should be included in the log message.
* <p>Should be configured using an {@code <init-param>} for parameter name
* "includePayload" in the filter definition in {@code web.xml}.
@@ -276,6 +297,10 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
}
}
+ if (isIncludeHeaders()) {
+ msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
+ }
+
if (isIncludePayload()) {
ContentCachingRequestWrapper wrapper =
WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
diff --git a/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java
index 56a7c899..fc564f3f 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -46,7 +46,9 @@ public class CharacterEncodingFilter extends OncePerRequestFilter {
private String encoding;
- private boolean forceEncoding = false;
+ private boolean forceRequestEncoding = false;
+
+ private boolean forceResponseEncoding = false;
/**
@@ -77,9 +79,26 @@ public class CharacterEncodingFilter extends OncePerRequestFilter {
* @see #setForceEncoding
*/
public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
+ this(encoding, forceEncoding, forceEncoding);
+ }
+
+ /**
+ * Create a {@code CharacterEncodingFilter} for the given encoding.
+ * @param encoding the encoding to apply
+ * @param forceRequestEncoding whether the specified encoding is supposed to
+ * override existing request encodings
+ * @param forceResponseEncoding whether the specified encoding is supposed to
+ * override existing response encodings
+ * @since 4.3
+ * @see #setEncoding
+ * @see #setForceRequestEncoding(boolean)
+ * @see #setForceResponseEncoding(boolean)
+ */
+ public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
Assert.hasLength(encoding, "Encoding must not be empty");
this.encoding = encoding;
- this.forceEncoding = forceEncoding;
+ this.forceRequestEncoding = forceRequestEncoding;
+ this.forceResponseEncoding = forceResponseEncoding;
}
@@ -95,15 +114,69 @@ public class CharacterEncodingFilter extends OncePerRequestFilter {
}
/**
+ * Return the configured encoding for requests and/or responses
+ * @since 4.3
+ */
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ /**
* Set whether the configured {@link #setEncoding encoding} of this filter
* is supposed to override existing request and response encodings.
* <p>Default is "false", i.e. do not modify the encoding if
* {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()}
* returns a non-null value. Switch this to "true" to enforce the specified
* encoding in any case, applying it as default response encoding as well.
+ * <p>This is the equivalent to setting both {@link #setForceRequestEncoding(boolean)}
+ * and {@link #setForceResponseEncoding(boolean)}.
+ * @see #setForceRequestEncoding(boolean)
+ * @see #setForceResponseEncoding(boolean)
*/
public void setForceEncoding(boolean forceEncoding) {
- this.forceEncoding = forceEncoding;
+ this.forceRequestEncoding = forceEncoding;
+ this.forceResponseEncoding = forceEncoding;
+ }
+
+ /**
+ * Set whether the configured {@link #setEncoding encoding} of this filter
+ * is supposed to override existing request encodings.
+ * <p>Default is "false", i.e. do not modify the encoding if
+ * {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()}
+ * returns a non-null value. Switch this to "true" to enforce the specified
+ * encoding in any case.
+ * @since 4.3
+ */
+ public void setForceRequestEncoding(boolean forceRequestEncoding) {
+ this.forceRequestEncoding = forceRequestEncoding;
+ }
+
+ /**
+ * Return whether the encoding should be forced on requests
+ * @since 4.3
+ */
+ public boolean isForceRequestEncoding() {
+ return this.forceRequestEncoding;
+ }
+
+ /**
+ * Set whether the configured {@link #setEncoding encoding} of this filter
+ * is supposed to override existing response encodings.
+ * <p>Default is "false", i.e. do not modify the encoding.
+ * Switch this to "true" to enforce the specified encoding
+ * for responses in any case.
+ * @since 4.3
+ */
+ public void setForceResponseEncoding(boolean forceResponseEncoding) {
+ this.forceResponseEncoding = forceResponseEncoding;
+ }
+
+ /**
+ * Return whether the encoding should be forced on responses.
+ * @since 4.3
+ */
+ public boolean isForceResponseEncoding() {
+ return this.forceResponseEncoding;
}
@@ -112,10 +185,13 @@ public class CharacterEncodingFilter extends OncePerRequestFilter {
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
- if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
- request.setCharacterEncoding(this.encoding);
- if (this.forceEncoding) {
- response.setCharacterEncoding(this.encoding);
+ String encoding = getEncoding();
+ if (encoding != null) {
+ if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
+ request.setCharacterEncoding(encoding);
+ }
+ if (isForceResponseEncoding()) {
+ response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java
new file mode 100644
index 00000000..a1196ec2
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2002-2016 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.web.filter;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpRequest;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.util.UrlPathHelper;
+
+/**
+ * Filter that wraps the request in order to override its
+ * {@link HttpServletRequest#getServerName() getServerName()},
+ * {@link HttpServletRequest#getServerPort() getServerPort()},
+ * {@link HttpServletRequest#getScheme() getScheme()}, and
+ * {@link HttpServletRequest#isSecure() isSecure()} methods with values derived
+ * from "Forwarded" or "X-Forwarded-*" headers. In effect the wrapped request
+ * reflects the client-originated protocol and address.
+ *
+ * @author Rossen Stoyanchev
+ * @author Eddú Meléndez
+ * @since 4.3
+ */
+public class ForwardedHeaderFilter extends OncePerRequestFilter {
+
+ private static final Set<String> FORWARDED_HEADER_NAMES =
+ Collections.newSetFromMap(new LinkedCaseInsensitiveMap<Boolean>(5, Locale.ENGLISH));
+
+ static {
+ FORWARDED_HEADER_NAMES.add("Forwarded");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Host");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Port");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
+ }
+
+
+ private final UrlPathHelper pathHelper = new UrlPathHelper();
+
+
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
+ Enumeration<String> names = request.getHeaderNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ if (FORWARDED_HEADER_NAMES.contains(name)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean shouldNotFilterAsyncDispatch() {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldNotFilterErrorDispatch() {
+ return false;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ filterChain.doFilter(new ForwardedHeaderRequestWrapper(request, this.pathHelper), response);
+ }
+
+
+ private static class ForwardedHeaderRequestWrapper extends HttpServletRequestWrapper {
+
+ private final String scheme;
+
+ private final boolean secure;
+
+ private final String host;
+
+ private final int port;
+
+ private final String contextPath;
+
+ private final String requestUri;
+
+ private final StringBuffer requestUrl;
+
+ private final Map<String, List<String>> headers;
+
+ public ForwardedHeaderRequestWrapper(HttpServletRequest request, UrlPathHelper pathHelper) {
+ super(request);
+
+ HttpRequest httpRequest = new ServletServerHttpRequest(request);
+ UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build();
+ int port = uriComponents.getPort();
+
+ this.scheme = uriComponents.getScheme();
+ this.secure = "https".equals(scheme);
+ this.host = uriComponents.getHost();
+ this.port = (port == -1 ? (this.secure ? 443 : 80) : port);
+
+ String prefix = getForwardedPrefix(request);
+ this.contextPath = (prefix != null ? prefix : request.getContextPath());
+ this.requestUri = this.contextPath + pathHelper.getPathWithinApplication(request);
+ this.requestUrl = new StringBuffer(this.scheme + "://" + this.host +
+ (port == -1 ? "" : ":" + port) + this.requestUri);
+ this.headers = initHeaders(request);
+ }
+
+ private static String getForwardedPrefix(HttpServletRequest request) {
+ String prefix = null;
+ Enumeration<String> names = request.getHeaderNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) {
+ prefix = request.getHeader(name);
+ }
+ }
+ if (prefix != null) {
+ while (prefix.endsWith("/")) {
+ prefix = prefix.substring(0, prefix.length() - 1);
+ }
+ }
+ return prefix;
+ }
+
+ /**
+ * Copy the headers excluding any {@link #FORWARDED_HEADER_NAMES}.
+ */
+ private static Map<String, List<String>> initHeaders(HttpServletRequest request) {
+ Map<String, List<String>> headers = new LinkedCaseInsensitiveMap<List<String>>(Locale.ENGLISH);
+ Enumeration<String> names = request.getHeaderNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ if (!FORWARDED_HEADER_NAMES.contains(name)) {
+ headers.put(name, Collections.list(request.getHeaders(name)));
+ }
+ }
+ return headers;
+ }
+
+ @Override
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ @Override
+ public String getServerName() {
+ return this.host;
+ }
+
+ @Override
+ public int getServerPort() {
+ return this.port;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ @Override
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
+ @Override
+ public String getRequestURI() {
+ return this.requestUri;
+ }
+
+ @Override
+ public StringBuffer getRequestURL() {
+ return this.requestUrl;
+ }
+
+ // Override header accessors to not expose forwarded headers
+
+ @Override
+ public String getHeader(String name) {
+ List<String> value = this.headers.get(name);
+ return (CollectionUtils.isEmpty(value) ? null : value.get(0));
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ List<String> value = this.headers.get(name);
+ return (Collections.enumeration(value != null ? value : Collections.<String>emptySet()));
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames() {
+ return Collections.enumeration(this.headers.keySet());
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
index b3fbe838..c94791a5 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -19,6 +19,7 @@ package org.springframework.web.filter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
+
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
@@ -60,11 +61,28 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING";
-
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
private static final boolean servlet3Present =
ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class);
+ private boolean writeWeakETag = false;
+
+ /**
+ * Set whether the ETag value written to the response should be weak, as per rfc7232.
+ * <p>Should be configured using an {@code <init-param>} for parameter name
+ * "writeWeakETag" in the filter definition in {@code web.xml}.
+ * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">rfc7232 section-2.3</a>
+ */
+ public boolean isWriteWeakETag() {
+ return writeWeakETag;
+ }
+
+ /**
+ * Return whether the ETag value written to the response should be weak, as per rfc7232.
+ */
+ public void setWriteWeakETag(boolean writeWeakETag) {
+ this.writeWeakETag = writeWeakETag;
+ }
/**
* The default value is "false" so that the filter may delay the generation of
@@ -102,10 +120,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
responseWrapper.copyBodyToResponse();
}
else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) {
- String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream());
+ String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream(), this.writeWeakETag);
rawResponse.setHeader(HEADER_ETAG, responseETag);
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
- if (responseETag.equals(requestETag)) {
+ if (requestETag != null
+ && (responseETag.equals(requestETag)
+ || responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", ""))
+ || "*".equals(requestETag))) {
if (logger.isTraceEnabled()) {
logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304");
}
@@ -144,7 +165,10 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response,
int responseStatusCode, InputStream inputStream) {
- if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(request.getMethod())) {
+ String method = request.getMethod();
+ if (responseStatusCode >= 200 && responseStatusCode < 300 &&
+ (HttpMethod.GET.matches(method) || HttpMethod.HEAD.matches(method))) {
+
String cacheControl = null;
if (servlet3Present) {
cacheControl = response.getHeader(HEADER_CACHE_CONTROL);
@@ -160,11 +184,17 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
* Generate the ETag header value from the given response body byte array.
* <p>The default implementation generates an MD5 hash.
* @param inputStream the response body as an InputStream
+ * @param isWeak whether the generated ETag should be weak
* @return the ETag header value
* @see org.springframework.util.DigestUtils
*/
- protected String generateETagHeaderValue(InputStream inputStream) throws IOException {
- StringBuilder builder = new StringBuilder("\"0");
+ protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
+ // length of W/ + 0 + " + 32bits md5 hash + "
+ StringBuilder builder = new StringBuilder(37);
+ if (isWeak) {
+ builder.append("W/");
+ }
+ builder.append("\"0");
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
builder.append('"');
return builder.toString();
diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java
index 0f857952..1567b39b 100644
--- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java
+++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -28,6 +28,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.OrderUtils;
import org.springframework.util.Assert;
@@ -102,7 +103,9 @@ public class ControllerAdviceBean implements Ordered {
this.order = initOrderFromBean(bean);
}
- ControllerAdvice annotation = AnnotationUtils.findAnnotation(beanType, ControllerAdvice.class);
+ ControllerAdvice annotation =
+ AnnotatedElementUtils.findMergedAnnotation(beanType, ControllerAdvice.class);
+
if (annotation != null) {
this.basePackages = initBasePackages(annotation);
this.assignableTypes = Arrays.asList(annotation.assignableTypes());
diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java
index 33a9b291..74f99557 100644
--- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java
+++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -226,7 +226,7 @@ public class HandlerMethod {
* if no annotation can be found on the given method itself.
* <p>Also supports <em>merged</em> composed annotations with attribute
* overrides as of Spring Framework 4.2.2.
- * @param annotationType the type of annotation to introspect the method for.
+ * @param annotationType the type of annotation to introspect the method for
* @return the annotation, or {@code null} if none found
* @see AnnotatedElementUtils#findMergedAnnotation
*/
@@ -235,6 +235,16 @@ public class HandlerMethod {
}
/**
+ * Return whether the parameter is declared with the given annotation type.
+ * @param annotationType the annotation type to look for
+ * @since 4.3
+ * @see AnnotatedElementUtils#hasAnnotation
+ */
+ public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
+ return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
+ }
+
+ /**
* If the provided instance contains a bean name rather than an object instance,
* the bean name is resolved before a {@link HandlerMethod} is created and returned.
*/
@@ -247,6 +257,15 @@ public class HandlerMethod {
return new HandlerMethod(this, handler);
}
+ /**
+ * Return a short representation of this handler method for log message purposes.
+ * @since 4.3
+ */
+ public String getShortLogMessage() {
+ int args = this.method.getParameterTypes().length;
+ return getBeanType().getName() + "#" + this.method.getName() + "[" + args + " args]";
+ }
+
@Override
public boolean equals(Object other) {
@@ -280,6 +299,10 @@ public class HandlerMethod {
super(HandlerMethod.this.bridgedMethod, index);
}
+ protected HandlerMethodParameter(HandlerMethodParameter original) {
+ super(original);
+ }
+
@Override
public Class<?> getContainingClass() {
return HandlerMethod.this.getBeanType();
@@ -289,6 +312,16 @@ public class HandlerMethod {
public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
return HandlerMethod.this.getMethodAnnotation(annotationType);
}
+
+ @Override
+ public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
+ return HandlerMethod.this.hasMethodAnnotation(annotationType);
+ }
+
+ @Override
+ public HandlerMethodParameter clone() {
+ return new HandlerMethodParameter(this);
+ }
}
@@ -304,10 +337,20 @@ public class HandlerMethod {
this.returnValue = returnValue;
}
+ protected ReturnValueMethodParameter(ReturnValueMethodParameter original) {
+ super(original);
+ this.returnValue = original.returnValue;
+ }
+
@Override
public Class<?> getParameterType() {
return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
}
+
+ @Override
+ public ReturnValueMethodParameter clone() {
+ return new ReturnValueMethodParameter(this);
+ }
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java
index 82acd2c3..4fc0545b 100644
--- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java
+++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java
@@ -39,7 +39,7 @@ public abstract class HandlerMethodSelector {
* @param handlerType the handler type to search handler methods on
* @param handlerMethodFilter a {@link MethodFilter} to help recognize handler methods of interest
* @return the selected methods, or an empty set
- * @see MethodIntrospector#selectMethods(Class, MethodFilter)
+ * @see MethodIntrospector#selectMethods
*/
public static Set<Method> selectMethods(Class<?> handlerType, MethodFilter handlerMethodFilter) {
return MethodIntrospector.selectMethods(handlerType, handlerMethodFilter);
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java
index aa724396..9d938367 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -63,7 +63,7 @@ public abstract class AbstractCookieValueMethodArgumentResolver extends Abstract
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
throw new ServletRequestBindingException("Missing cookie '" + name +
- "' for method parameter of type " + parameter.getParameterType().getSimpleName());
+ "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName());
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java
index 78a3a345..e2e92d64 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
+import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -53,6 +54,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.1
*/
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
@@ -61,7 +63,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
private final BeanExpressionContext expressionContext;
- private Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<MethodParameter, NamedValueInfo>(256);
+ private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<MethodParameter, NamedValueInfo>(256);
public AbstractNamedValueMethodArgumentResolver() {
@@ -84,27 +86,33 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
- Class<?> paramType = parameter.getParameterType();
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
+ MethodParameter nestedParameter = parameter.nestedIfOptional();
- Object arg = resolveName(namedValueInfo.name, parameter, webRequest);
+ Object resolvedName = resolveStringValue(namedValueInfo.name);
+ if (resolvedName == null) {
+ throw new IllegalArgumentException(
+ "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
+ }
+
+ Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
- arg = resolveDefaultValue(namedValueInfo.defaultValue);
+ arg = resolveStringValue(namedValueInfo.defaultValue);
}
- else if (namedValueInfo.required && !parameter.getParameterType().getName().equals("java.util.Optional")) {
- handleMissingValue(namedValueInfo.name, parameter);
+ else if (namedValueInfo.required && !nestedParameter.isOptional()) {
+ handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
- arg = handleNullValue(namedValueInfo.name, arg, paramType);
+ arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
- arg = resolveDefaultValue(namedValueInfo.defaultValue);
+ arg = resolveStringValue(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
- arg = binder.convertIfNecessary(arg, paramType, parameter);
+ arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
@@ -151,7 +159,8 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
if (info.name.length() == 0) {
name = parameter.getParameterName();
if (name == null) {
- throw new IllegalArgumentException("Name for argument type [" + parameter.getParameterType().getName() +
+ throw new IllegalArgumentException(
+ "Name for argument type [" + parameter.getNestedParameterType().getName() +
"] not available, and parameter name information not found in class file either.");
}
}
@@ -160,29 +169,43 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
}
/**
- * Resolves the given parameter type and value name into an argument value.
+ * Resolve the given annotation-specified value,
+ * potentially containing placeholders and expressions.
+ */
+ private Object resolveStringValue(String value) {
+ if (this.configurableBeanFactory == null) {
+ return value;
+ }
+ String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
+ BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
+ if (exprResolver == null) {
+ return value;
+ }
+ return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
+ }
+
+ /**
+ * Resolve the given parameter type and value name into an argument value.
* @param name the name of the value being resolved
* @param parameter the method parameter to resolve to an argument value
+ * (pre-nested in case of a {@link java.util.Optional} declaration)
* @param request the current request
- * @return the resolved argument. May be {@code null}
+ * @return the resolved argument (may be {@code null})
* @throws Exception in case of errors
*/
protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request)
throws Exception;
/**
- * Resolves the given default value into an argument value.
+ * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)}
+ * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case.
+ * @param name the name for the value
+ * @param parameter the method parameter
+ * @param request the current request
+ * @since 4.3
*/
- private Object resolveDefaultValue(String defaultValue) {
- if (this.configurableBeanFactory == null) {
- return defaultValue;
- }
- String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(defaultValue);
- BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
- if (exprResolver == null) {
- return defaultValue;
- }
- return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
+ protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
+ handleMissingValue(name, parameter);
}
/**
@@ -191,7 +214,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
* @param name the name for the value
* @param parameter the method parameter
*/
- protected abstract void handleMissingValue(String name, MethodParameter parameter) throws ServletException;
+ protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
+ throw new ServletRequestBindingException("Missing argument '" + name +
+ "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName());
+ }
/**
* A {@code null} results in a {@code false} value for {@code boolean}s or an exception for other primitives.
@@ -202,7 +228,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
return Boolean.FALSE;
}
else if (paramType.isPrimitive()) {
- throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
+ throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name +
"' is present but cannot be translated into a null value due to being declared as a " +
"primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java
index 5d307cba..c121f575 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
* to the exception types supported by a given {@link Method}.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.1
*/
public class ExceptionHandlerMethodResolver {
@@ -98,8 +99,8 @@ public class ExceptionHandlerMethodResolver {
}
protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
- ExceptionHandler annot = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
- result.addAll(Arrays.asList(annot.value()));
+ ExceptionHandler ann = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
+ result.addAll(Arrays.asList(ann.value()));
}
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
@@ -124,7 +125,14 @@ public class ExceptionHandlerMethodResolver {
* @return a Method to handle the exception, or {@code null} if none found
*/
public Method resolveMethod(Exception exception) {
- return resolveMethodByExceptionType(exception.getClass());
+ Method method = resolveMethodByExceptionType(exception.getClass());
+ if (method == null) {
+ Throwable cause = exception.getCause();
+ if (cause != null) {
+ method = resolveMethodByExceptionType(cause.getClass());
+ }
+ }
+ return method;
}
/**
@@ -133,7 +141,7 @@ public class ExceptionHandlerMethodResolver {
* @param exceptionType the exception type
* @return a Method to handle the exception, or {@code null} if none found
*/
- public Method resolveMethodByExceptionType(Class<? extends Exception> exceptionType) {
+ public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
method = getMappedMethod(exceptionType);
@@ -145,7 +153,7 @@ public class ExceptionHandlerMethodResolver {
/**
* Return the {@link Method} mapped to the given exception type, or {@code null} if none.
*/
- private Method getMappedMethod(Class<? extends Exception> exceptionType) {
+ private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
index 72e43cd1..8c82eefd 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -38,23 +38,24 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
- * Resolves method arguments annotated with {@code @ModelAttribute} and handles
- * return values from methods annotated with {@code @ModelAttribute}.
+ * Resolve {@code @ModelAttribute} annotated method arguments and handle
+ * return values from {@code @ModelAttribute} annotated methods.
*
- * <p>Model attributes are obtained from the model or if not found possibly
- * created with a default constructor if it is available. Once created, the
- * attributed is populated with request data via data binding and also
- * validation may be applied if the argument is annotated with
- * {@code @javax.validation.Valid}.
+ * <p>Model attributes are obtained from the model or created with a default
+ * constructor (and then added to the model). Once created the attribute is
+ * populated via data binding to Servlet request parameters. Validation may be
+ * applied if the argument is annotated with {@code @javax.validation.Valid}.
+ * or Spring's own {@code @org.springframework.validation.annotation.Validated}.
*
- * <p>When this handler is created with {@code annotationNotRequired=true},
+ * <p>When this handler is created with {@code annotationNotRequired=true}
* any non-simple type argument and return value is regarded as a model
* attribute with or without the presence of an {@code @ModelAttribute}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
-public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
+public class ModelAttributeMethodProcessor
+ implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
protected final Log logger = LogFactory.getLog(getClass());
@@ -62,6 +63,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
/**
+ * Class constructor.
* @param annotationNotRequired if "true", non-simple method arguments and
* return values are considered model attributes with or without a
* {@code @ModelAttribute} annotation.
@@ -72,20 +74,14 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
/**
- * Returns {@code true} if the parameter is annotated with {@link ModelAttribute}
- * or in default resolution mode, and also if it is not a simple type.
+ * Returns {@code true} if the parameter is annotated with
+ * {@link ModelAttribute} or, if in default resolution mode, for any
+ * method parameter that is not a simple type.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
- if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
- return true;
- }
- else if (this.annotationNotRequired) {
- return !BeanUtils.isSimpleProperty(parameter.getParameterType());
- }
- else {
- return false;
- }
+ return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
+ (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
/**
@@ -102,12 +98,21 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String name = ModelFactory.getNameForParameter(parameter);
- Object attribute = (mavContainer.containsAttribute(name) ?
- mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest));
+ Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
+ createAttribute(name, parameter, binderFactory, webRequest));
+
+ if (!mavContainer.isBindingDisabled(name)) {
+ ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
+ if (ann != null && !ann.binding()) {
+ mavContainer.setBindingDisabled(name);
+ }
+ }
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
- bindRequestParameters(binder, webRequest);
+ if (!mavContainer.isBindingDisabled(name)) {
+ bindRequestParameters(binder, webRequest);
+ }
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
@@ -182,19 +187,13 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
/**
* Return {@code true} if there is a method-level {@code @ModelAttribute}
- * or if it is a non-simple type when {@code annotationNotRequired=true}.
+ * or, in default resolution mode, for any return value type that is not
+ * a simple type.
*/
@Override
public boolean supportsReturnType(MethodParameter returnType) {
- if (returnType.getMethodAnnotation(ModelAttribute.class) != null) {
- return true;
- }
- else if (this.annotationNotRequired) {
- return !BeanUtils.isSimpleProperty(returnType.getParameterType());
- }
- else {
- return false;
- }
+ return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
+ (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
}
/**
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java
index 9eb31c0d..be4472d3 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java
@@ -112,7 +112,7 @@ public final class ModelFactory {
if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
- throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
+ throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
}
container.addAttribute(name, value);
}
@@ -128,14 +128,20 @@ public final class ModelFactory {
while (!this.modelMethods.isEmpty()) {
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
- String modelName = modelMethod.getMethodAnnotation(ModelAttribute.class).value();
- if (container.containsAttribute(modelName)) {
+ ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
+ if (container.containsAttribute(ann.name())) {
+ if (!ann.binding()) {
+ container.setBindingDisabled(ann.name());
+ }
continue;
}
Object returnValue = modelMethod.invokeForRequest(request, container);
if (!modelMethod.isVoid()){
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
+ if (!ann.binding()) {
+ container.setBindingDisabled(returnValueName);
+ }
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java
index 3ef1b4fe..c8575251 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java
@@ -56,7 +56,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(RequestHeader.class) &&
- !Map.class.isAssignableFrom(parameter.getParameterType()));
+ !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()));
}
@Override
@@ -79,7 +79,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
throw new ServletRequestBindingException("Missing request header '" + name +
- "' for method parameter of type " + parameter.getParameterType().getSimpleName());
+ "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName());
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java
index c481b152..bc065619 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java
@@ -17,22 +17,17 @@
package org.springframework.web.method.annotation;
import java.beans.PropertyEditor;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.Part;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
-import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
-import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
@@ -45,6 +40,8 @@ import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
+import org.springframework.web.multipart.support.MissingServletRequestPartException;
+import org.springframework.web.multipart.support.MultipartResolutionDelegate;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.WebUtils;
@@ -85,7 +82,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
/**
* @param useDefaultResolution in default resolution mode a method argument
* that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
- * is treated as a request parameter even if it it isn't annotated, the
+ * is treated as a request parameter even if it isn't annotated, the
* request parameter name is derived from the method parameter name.
*/
public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
@@ -98,7 +95,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
* values are not expected to contain expressions
* @param useDefaultResolution in default resolution mode a method argument
* that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
- * is treated as a request parameter even if it it isn't annotated, the
+ * is treated as a request parameter even if it isn't annotated, the
* request parameter name is derived from the method parameter name.
*/
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
@@ -124,9 +121,8 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
- Class<?> paramType = parameter.getParameterType();
if (parameter.hasParameterAnnotation(RequestParam.class)) {
- if (Map.class.isAssignableFrom(paramType)) {
+ if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
String paramName = parameter.getParameterAnnotation(RequestParam.class).name();
return StringUtils.hasText(paramName);
}
@@ -138,11 +134,12 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
- else if (MultipartFile.class == paramType || "javax.servlet.http.Part".equals(paramType.getName())) {
+ parameter = parameter.nestedIfOptional();
+ if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
- return BeanUtils.isSimpleProperty(paramType);
+ return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
@@ -157,105 +154,53 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
}
@Override
- protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
- HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
+ protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
+ HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
- Object arg;
- if (MultipartFile.class == parameter.getParameterType()) {
- assertIsMultipartRequest(servletRequest);
- Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
- arg = multipartRequest.getFile(name);
+ Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
+ if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
+ return mpArg;
}
- else if (isMultipartFileCollection(parameter)) {
- assertIsMultipartRequest(servletRequest);
- Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
- arg = multipartRequest.getFiles(name);
- }
- else if (isMultipartFileArray(parameter)) {
- assertIsMultipartRequest(servletRequest);
- Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
- List<MultipartFile> multipartFiles = multipartRequest.getFiles(name);
- arg = multipartFiles.toArray(new MultipartFile[multipartFiles.size()]);
- }
- else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {
- assertIsMultipartRequest(servletRequest);
- arg = servletRequest.getPart(name);
- }
- else if (isPartCollection(parameter)) {
- assertIsMultipartRequest(servletRequest);
- arg = new ArrayList<Object>(servletRequest.getParts());
- }
- else if (isPartArray(parameter)) {
- assertIsMultipartRequest(servletRequest);
- arg = RequestPartResolver.resolvePart(servletRequest);
- }
- else {
- arg = null;
- if (multipartRequest != null) {
- List<MultipartFile> files = multipartRequest.getFiles(name);
- if (!files.isEmpty()) {
- arg = (files.size() == 1 ? files.get(0) : files);
- }
+
+ Object arg = null;
+ if (multipartRequest != null) {
+ List<MultipartFile> files = multipartRequest.getFiles(name);
+ if (!files.isEmpty()) {
+ arg = (files.size() == 1 ? files.get(0) : files);
}
- if (arg == null) {
- String[] paramValues = webRequest.getParameterValues(name);
- if (paramValues != null) {
- arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
- }
+ }
+ if (arg == null) {
+ String[] paramValues = request.getParameterValues(name);
+ if (paramValues != null) {
+ arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
-
return arg;
}
- private void assertIsMultipartRequest(HttpServletRequest request) {
- String contentType = request.getContentType();
- if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) {
- throw new MultipartException("The current request is not a multipart request");
- }
- }
-
- private boolean isMultipartFileCollection(MethodParameter parameter) {
- return (MultipartFile.class == getCollectionParameterType(parameter));
- }
-
- private boolean isMultipartFileArray(MethodParameter parameter) {
- return (MultipartFile.class == parameter.getParameterType().getComponentType());
- }
-
- private boolean isPartCollection(MethodParameter parameter) {
- Class<?> collectionType = getCollectionParameterType(parameter);
- return (collectionType != null && "javax.servlet.http.Part".equals(collectionType.getName()));
- }
-
- private boolean isPartArray(MethodParameter parameter) {
- Class<?> paramType = parameter.getParameterType().getComponentType();
- return (paramType != null && "javax.servlet.http.Part".equals(paramType.getName()));
- }
-
- private Class<?> getCollectionParameterType(MethodParameter parameter) {
- Class<?> paramType = parameter.getParameterType();
- if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){
- Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(parameter);
- if (valueType != null) {
- return valueType;
+ @Override
+ protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
+ HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
+ if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
+ if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
+ throw new MultipartException("Current request is not a multipart request");
+ }
+ else {
+ throw new MissingServletRequestPartException(name);
}
}
- return null;
- }
-
- @Override
- protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
- throw new MissingServletRequestParameterException(name, parameter.getParameterType().getSimpleName());
+ else {
+ throw new MissingServletRequestParameterException(name, parameter.getNestedParameterType().getSimpleName());
+ }
}
@Override
public void contributeMethodArgument(MethodParameter parameter, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
- Class<?> paramType = parameter.getParameterType();
+ Class<?> paramType = parameter.getNestedParameterType();
if (Map.class.isAssignableFrom(paramType) || MultipartFile.class == paramType ||
"javax.servlet.http.Part".equals(paramType.getName())) {
return;
@@ -266,6 +211,11 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
parameter.getParameterName() : requestParam.name());
if (value == null) {
+ if (requestParam != null) {
+ if (!requestParam.required() || !requestParam.defaultValue().equals(ValueConstants.DEFAULT_NONE)) {
+ return;
+ }
+ }
builder.queryParam(name);
}
else if (value instanceof Collection) {
@@ -306,12 +256,4 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
}
}
-
- private static class RequestPartResolver {
-
- public static Object resolvePart(HttpServletRequest servletRequest) throws Exception {
- return servletRequest.getParts().toArray(new Part[servletRequest.getParts().size()]);
- }
- }
-
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java
index 3ff2b315..8cc07e46 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -24,7 +24,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionAttributeStore;
@@ -65,10 +65,11 @@ public class SessionAttributesHandler {
* @param sessionAttributeStore used for session access
*/
public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) {
- Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null.");
+ Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null");
this.sessionAttributeStore = sessionAttributeStore;
- SessionAttributes annotation = AnnotationUtils.findAnnotation(handlerType, SessionAttributes.class);
+ SessionAttributes annotation =
+ AnnotatedElementUtils.findMergedAnnotation(handlerType, SessionAttributes.class);
if (annotation != null) {
this.attributeNames.addAll(Arrays.asList(annotation.names()));
this.attributeTypes.addAll(Arrays.asList(annotation.types()));
@@ -84,7 +85,7 @@ public class SessionAttributesHandler {
* session attributes through an {@link SessionAttributes} annotation.
*/
public boolean hasSessionAttributes() {
- return ((this.attributeNames.size() > 0) || (this.attributeTypes.size() > 0));
+ return (this.attributeNames.size() > 0 || this.attributeTypes.size() > 0);
}
/**
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java
index 5492582e..e80655a9 100644
--- a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java
+++ b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java
@@ -34,6 +34,7 @@ import org.springframework.web.context.request.NativeWebRequest;
* Previously resolved method parameters are cached for faster lookups.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.1
*/
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
@@ -57,6 +58,19 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
/**
* Add the given {@link HandlerMethodArgumentResolver}s.
+ * @since 4.3
+ */
+ public HandlerMethodArgumentResolverComposite addResolvers(HandlerMethodArgumentResolver... resolvers) {
+ if (resolvers != null) {
+ for (HandlerMethodArgumentResolver resolver : resolvers) {
+ this.argumentResolvers.add(resolver);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Add the given {@link HandlerMethodArgumentResolver}s.
*/
public HandlerMethodArgumentResolverComposite addResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
if (resolvers != null) {
@@ -74,6 +88,14 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
return Collections.unmodifiableList(this.argumentResolvers);
}
+ /**
+ * Clear the list of configured resolvers.
+ * @since 4.3
+ */
+ public void clear() {
+ this.argumentResolvers.clear();
+ }
+
/**
* Whether the given {@linkplain MethodParameter method parameter} is supported by any registered
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java b/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java
index 536d53b0..ec976824 100644
--- a/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java
+++ b/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,8 +16,11 @@
package org.springframework.web.method.support;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.support.BindingAwareModelMap;
@@ -54,6 +57,11 @@ public class ModelAndViewContainer {
private boolean redirectModelScenario = false;
+ /* Names of attributes with binding disabled */
+ private final Set<String> bindingDisabledAttributes = new HashSet<String>(4);
+
+ private HttpStatus status;
+
private final SessionStatus sessionStatus = new SimpleSessionStatus();
private boolean requestHandled = false;
@@ -134,6 +142,24 @@ public class ModelAndViewContainer {
}
/**
+ * Register an attribute for which data binding should not occur, for example
+ * corresponding to an {@code @ModelAttribute(binding=false)} declaration.
+ * @param attributeName the name of the attribute
+ * @since 4.3
+ */
+ public void setBindingDisabled(String attributeName) {
+ this.bindingDisabledAttributes.add(attributeName);
+ }
+
+ /**
+ * Whether binding is disabled for the given model attribute.
+ * @since 4.3
+ */
+ public boolean isBindingDisabled(String name) {
+ return this.bindingDisabledAttributes.contains(name);
+ }
+
+ /**
* Whether to use the default model or the redirect model.
*/
private boolean useDefaultModel() {
@@ -156,7 +182,7 @@ public class ModelAndViewContainer {
/**
* Provide a separate model instance to use in a redirect scenario.
- * The provided additional model however is not used used unless
+ * The provided additional model however is not used unless
* {@link #setRedirectModelScenario(boolean)} gets set to {@code true} to signal
* a redirect scenario.
*/
@@ -181,6 +207,23 @@ public class ModelAndViewContainer {
}
/**
+ * Provide a HTTP status that will be passed on to with the
+ * {@code ModelAndView} used for view rendering purposes.
+ * @since 4.3
+ */
+ public void setStatus(HttpStatus status) {
+ this.status = status;
+ }
+
+ /**
+ * Return the configured HTTP status, if any.
+ * @since 4.3
+ */
+ public HttpStatus getStatus() {
+ return this.status;
+ }
+
+ /**
* Whether the request has been handled fully within the handler, e.g.
* {@code @ResponseBody} method, and therefore view resolution is not
* necessary. This flag can also be set when controller methods declare an
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java
index 622b1844..727bc92c 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java
@@ -304,7 +304,7 @@ public abstract class CommonsFileUploadSupport {
return defaultEncoding;
}
MediaType contentType = MediaType.parseMediaType(contentTypeHeader);
- Charset charset = contentType.getCharSet();
+ Charset charset = contentType.getCharset();
return (charset != null ? charset.name() : defaultEncoding);
}
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java
index b9ac26ed..577b4e3b 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -39,6 +39,10 @@ import org.springframework.web.multipart.MultipartResolver;
* <p>If no MultipartResolver bean is found, this filter falls back to a default
* MultipartResolver: {@link StandardServletMultipartResolver} for Servlet 3.0,
* based on a multipart-config section in {@code web.xml}.
+ * Note however that at present the Servlet specification only defines how to
+ * enable multipart configuration on a Servlet and as a result multipart request
+ * processing is likely not possible in a Filter unless the Servlet container
+ * provides a workaround such as Tomcat's "allowCasualMultipartParsing" property.
*
* <p>MultipartResolver lookup is customizable: Override this filter's
* {@code lookupMultipartResolver} method to use a custom MultipartResolver
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java
new file mode 100644
index 00000000..42248276
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2002-2016 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.web.multipart.support;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.Part;
+
+import org.springframework.core.GenericCollectionTypeResolver;
+import org.springframework.core.MethodParameter;
+import org.springframework.util.ClassUtils;
+import org.springframework.web.multipart.MultipartException;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * A common delegate for {@code HandlerMethodArgumentResolver} implementations
+ * which need to resolve {@link MultipartFile} and {@link Part} arguments.
+ *
+ * @author Juergen Hoeller
+ * @since 4.3
+ */
+public abstract class MultipartResolutionDelegate {
+
+ public static final Object UNRESOLVABLE = new Object();
+
+
+ private static Class<?> servletPartClass = null;
+
+ static {
+ try {
+ servletPartClass = ClassUtils.forName("javax.servlet.http.Part",
+ MultipartResolutionDelegate.class.getClassLoader());
+ }
+ catch (ClassNotFoundException ex) {
+ // Servlet 3.0 javax.servlet.http.Part type not available -
+ // Part references simply not supported then.
+ }
+ }
+
+
+ public static boolean isMultipartRequest(HttpServletRequest request) {
+ return (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null ||
+ isMultipartContent(request));
+ }
+
+ private static boolean isMultipartContent(HttpServletRequest request) {
+ String contentType = request.getContentType();
+ return (contentType != null && contentType.toLowerCase().startsWith("multipart/"));
+ }
+
+ static MultipartHttpServletRequest asMultipartHttpServletRequest(HttpServletRequest request) {
+ MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
+ if (unwrapped != null) {
+ return unwrapped;
+ }
+ return adaptToMultipartHttpServletRequest(request);
+ }
+
+ private static MultipartHttpServletRequest adaptToMultipartHttpServletRequest(HttpServletRequest request) {
+ if (servletPartClass != null) {
+ // Servlet 3.0 available ..
+ return new StandardMultipartHttpServletRequest(request);
+ }
+ throw new MultipartException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
+ }
+
+
+ public static boolean isMultipartArgument(MethodParameter parameter) {
+ Class<?> paramType = parameter.getNestedParameterType();
+ return (MultipartFile.class == paramType ||
+ isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) ||
+ (servletPartClass != null && (servletPartClass == paramType ||
+ isPartCollection(parameter) || isPartArray(parameter))));
+ }
+
+ public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
+ throws Exception {
+
+ MultipartHttpServletRequest multipartRequest =
+ WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
+ boolean isMultipart = (multipartRequest != null || isMultipartContent(request));
+
+ if (MultipartFile.class == parameter.getNestedParameterType()) {
+ if (multipartRequest == null && isMultipart) {
+ multipartRequest = adaptToMultipartHttpServletRequest(request);
+ }
+ return (multipartRequest != null ? multipartRequest.getFile(name) : null);
+ }
+ else if (isMultipartFileCollection(parameter)) {
+ if (multipartRequest == null && isMultipart) {
+ multipartRequest = adaptToMultipartHttpServletRequest(request);
+ }
+ return (multipartRequest != null ? multipartRequest.getFiles(name) : null);
+ }
+ else if (isMultipartFileArray(parameter)) {
+ if (multipartRequest == null && isMultipart) {
+ multipartRequest = adaptToMultipartHttpServletRequest(request);
+ }
+ if (multipartRequest != null) {
+ List<MultipartFile> multipartFiles = multipartRequest.getFiles(name);
+ return multipartFiles.toArray(new MultipartFile[multipartFiles.size()]);
+ }
+ else {
+ return null;
+ }
+ }
+ else if (servletPartClass != null) {
+ if (servletPartClass == parameter.getNestedParameterType()) {
+ return (isMultipart ? RequestPartResolver.resolvePart(request, name) : null);
+ }
+ else if (isPartCollection(parameter)) {
+ return (isMultipart ? RequestPartResolver.resolvePartList(request, name) : null);
+ }
+ else if (isPartArray(parameter)) {
+ return (isMultipart ? RequestPartResolver.resolvePartArray(request, name) : null);
+ }
+ }
+ return UNRESOLVABLE;
+ }
+
+ private static boolean isMultipartFileCollection(MethodParameter methodParam) {
+ return (MultipartFile.class == getCollectionParameterType(methodParam));
+ }
+
+ private static boolean isMultipartFileArray(MethodParameter methodParam) {
+ return (MultipartFile.class == methodParam.getNestedParameterType().getComponentType());
+ }
+
+ private static boolean isPartCollection(MethodParameter methodParam) {
+ return (servletPartClass == getCollectionParameterType(methodParam));
+ }
+
+ private static boolean isPartArray(MethodParameter methodParam) {
+ return (servletPartClass == methodParam.getNestedParameterType().getComponentType());
+ }
+
+ private static Class<?> getCollectionParameterType(MethodParameter methodParam) {
+ Class<?> paramType = methodParam.getNestedParameterType();
+ if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){
+ Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam);
+ if (valueType != null) {
+ return valueType;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Inner class to avoid hard-coded dependency on Servlet 3.0 Part type...
+ */
+ private static class RequestPartResolver {
+
+ public static Object resolvePart(HttpServletRequest servletRequest, String name) throws Exception {
+ return servletRequest.getPart(name);
+ }
+
+ public static Object resolvePartList(HttpServletRequest servletRequest, String name) throws Exception {
+ Collection<Part> parts = servletRequest.getParts();
+ List<Part> result = new ArrayList<Part>(parts.size());
+ for (Part part : parts) {
+ if (part.getName().equals(name)) {
+ result.add(part);
+ }
+ }
+ return result;
+ }
+
+ public static Object resolvePartArray(HttpServletRequest servletRequest, String name) throws Exception {
+ Collection<Part> parts = servletRequest.getParts();
+ List<Part> result = new ArrayList<Part>(parts.size());
+ for (Part part : parts) {
+ if (part.getName().equals(name)) {
+ result.add(part);
+ }
+ }
+ return result.toArray(new Part[result.size()]);
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java
index 78eab918..ae817e4b 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java
@@ -26,12 +26,10 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
-import org.springframework.util.ClassUtils;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
-import org.springframework.web.util.WebUtils;
/**
* {@link ServerHttpRequest} implementation that accesses one part of a multipart
@@ -57,40 +55,20 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques
* @param request the current servlet request
* @param partName the name of the part to adapt to the {@link ServerHttpRequest} contract
* @throws MissingServletRequestPartException if the request part cannot be found
- * @throws IllegalArgumentException if MultipartHttpServletRequest cannot be initialized
+ * @throws MultipartException if MultipartHttpServletRequest cannot be initialized
*/
public RequestPartServletServerHttpRequest(HttpServletRequest request, String partName)
throws MissingServletRequestPartException {
super(request);
- this.multipartRequest = asMultipartRequest(request);
+ this.multipartRequest = MultipartResolutionDelegate.asMultipartHttpServletRequest(request);
this.partName = partName;
this.headers = this.multipartRequest.getMultipartHeaders(this.partName);
if (this.headers == null) {
- if (request instanceof MultipartHttpServletRequest) {
- throw new MissingServletRequestPartException(partName);
- }
- else {
- throw new IllegalArgumentException(
- "Failed to obtain request part: " + partName + ". " +
- "The part is missing or multipart processing is not configured. " +
- "Check for a MultipartResolver bean or if Servlet 3.0 multipart processing is enabled.");
- }
- }
- }
-
- private static MultipartHttpServletRequest asMultipartRequest(HttpServletRequest request) {
- MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
- if (unwrapped != null) {
- return unwrapped;
- }
- else if (ClassUtils.hasMethod(HttpServletRequest.class, "getParts")) {
- // Servlet 3.0 available ..
- return new StandardMultipartHttpServletRequest(request);
+ throw new MissingServletRequestPartException(partName);
}
- throw new IllegalArgumentException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
}
@@ -125,7 +103,7 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques
private String determineEncoding() {
MediaType contentType = getHeaders().getContentType();
if (contentType != null) {
- Charset charset = contentType.getCharSet();
+ Charset charset = contentType.getCharset();
if (charset != null) {
return charset.name();
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java
new file mode 100644
index 00000000..1323bc94
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2002-2016 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.web.util;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+
+/**
+ * Abstract base class for {@link UriTemplateHandler} implementations.
+ *
+ * <p>Support {@link #setBaseUrl} and {@link #setDefaultUriVariables} properties
+ * that should be relevant regardless of the URI template expand and encode
+ * mechanism used in sub-classes.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+public abstract class AbstractUriTemplateHandler implements UriTemplateHandler {
+
+ private String baseUrl;
+
+ private final Map<String, Object> defaultUriVariables = new HashMap<String, Object>();
+
+
+ /**
+ * Configure a base URL to prepend URI templates with. The base URL must
+ * have a scheme and host but may optionally contain a port and a path.
+ * The base URL must be fully expanded and encoded which can be done via
+ * {@link UriComponentsBuilder}.
+ * @param baseUrl the base URL.
+ */
+ public void setBaseUrl(String baseUrl) {
+ if (baseUrl != null) {
+ UriComponents uriComponents = UriComponentsBuilder.fromUriString(baseUrl).build();
+ Assert.hasText(uriComponents.getScheme(), "'baseUrl' must have a scheme");
+ Assert.hasText(uriComponents.getHost(), "'baseUrl' must have a host");
+ Assert.isNull(uriComponents.getQuery(), "'baseUrl' cannot have a query");
+ Assert.isNull(uriComponents.getFragment(), "'baseUrl' cannot have a fragment");
+ }
+ this.baseUrl = baseUrl;
+ }
+
+ /**
+ * Return the configured base URL.
+ */
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ /**
+ * Configure default URI variable values to use with every expanded URI
+ * template. These default values apply only when expanding with a Map, and
+ * not with an array, where the Map supplied to {@link #expand(String, Map)}
+ * can override the default values.
+ * @param defaultUriVariables the default URI variable values
+ * @since 4.3
+ */
+ public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) {
+ this.defaultUriVariables.clear();
+ if (defaultUriVariables != null) {
+ this.defaultUriVariables.putAll(defaultUriVariables);
+ }
+ }
+
+ /**
+ * Return a read-only copy of the configured default URI variables.
+ */
+ public Map<String, ?> getDefaultUriVariables() {
+ return Collections.unmodifiableMap(this.defaultUriVariables);
+ }
+
+
+ @Override
+ public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
+ if (!getDefaultUriVariables().isEmpty()) {
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.putAll(getDefaultUriVariables());
+ map.putAll(uriVariables);
+ uriVariables = map;
+ }
+ URI url = expandInternal(uriTemplate, uriVariables);
+ return insertBaseUrl(url);
+ }
+
+ @Override
+ public URI expand(String uriTemplate, Object... uriVariables) {
+ URI url = expandInternal(uriTemplate, uriVariables);
+ return insertBaseUrl(url);
+ }
+
+
+ /**
+ * Actually expand and encode the URI template.
+ */
+ protected abstract URI expandInternal(String uriTemplate, Map<String, ?> uriVariables);
+
+ /**
+ * Actually expand and encode the URI template.
+ */
+ protected abstract URI expandInternal(String uriTemplate, Object... uriVariables);
+
+
+ /**
+ * Insert a base URL (if configured) unless the given URL has a host already.
+ */
+ private URI insertBaseUrl(URI url) {
+ try {
+ String baseUrl = getBaseUrl();
+ if (baseUrl != null && url.getHost() == null) {
+ url = new URI(baseUrl + url.toString());
+ }
+ return url;
+ }
+ catch (URISyntaxException ex) {
+ throw new IllegalArgumentException("Invalid URL after inserting base URL: " + url, ex);
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java
index 7bc23949..214b97ad 100644
--- a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java
+++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java
@@ -138,7 +138,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
// Overrides Servlet 3.1 setContentLengthLong(long) at runtime
public void setContentLengthLong(long len) {
if (len > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" +
+ throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" +
Integer.MAX_VALUE + "): " + len);
}
int lenInt = (int) len;
diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
index e4f79832..1116df9e 100644
--- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,62 +16,39 @@
package org.springframework.web.util;
+import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.springframework.util.Assert;
-
/**
- * Default implementation of {@link UriTemplateHandler} that relies on
- * {@link UriComponentsBuilder} internally.
+ * Default implementation of {@link UriTemplateHandler} based on the use of
+ * {@link UriComponentsBuilder} for expanding and encoding variables.
+ *
+ * <p>There are also several properties to customize how URI template handling
+ * is performed, including a {@link #setBaseUrl baseUrl} to be used as a prefix
+ * for all URI templates and a couple of encoding related options &mdash;
+ * {@link #setParsePath parsePath} and {@link #setStrictEncoding strictEncoding}
+ * respectively.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
-public class DefaultUriTemplateHandler implements UriTemplateHandler {
-
- private String baseUrl;
+public class DefaultUriTemplateHandler extends AbstractUriTemplateHandler {
private boolean parsePath;
+ private boolean strictEncoding;
- /**
- * Configure a base URL to prepend URI templates with. The base URL should
- * have a scheme and host but may also contain a port and a partial path.
- * Individual URI templates then may provide the remaining part of the URL
- * including additional path, query and fragment.
- * <p><strong>Note: </strong>Individual URI templates are expanded and
- * encoded before being appended to the base URL. Therefore the base URL is
- * expected to be fully expanded and encoded, which can be done with the help
- * of {@link UriComponentsBuilder}.
- * @param baseUrl the base URL.
- */
- public void setBaseUrl(String baseUrl) {
- if (baseUrl != null) {
- UriComponents uriComponents = UriComponentsBuilder.fromUriString(baseUrl).build();
- Assert.hasText(uriComponents.getScheme(), "'baseUrl' must have a scheme");
- Assert.hasText(uriComponents.getHost(), "'baseUrl' must have a host");
- Assert.isNull(uriComponents.getQuery(), "'baseUrl' cannot have a query");
- Assert.isNull(uriComponents.getFragment(), "'baseUrl' cannot have a fragment");
- }
- this.baseUrl = baseUrl;
- }
-
- /**
- * Return the configured base URL.
- */
- public String getBaseUrl() {
- return this.baseUrl;
- }
/**
* Whether to parse the path of a URI template string into path segments.
- * <p>If set to {@code true} the path of parsed URI templates is decomposed
- * into path segments so that URI variables expanded into the path are
- * treated according to path segment encoding rules. In effect that means the
- * "/" character is percent encoded.
+ * <p>If set to {@code true} the URI template path is immediately decomposed
+ * into path segments any URI variables expanded into it are then subject to
+ * path segment encoding rules. In effect URI variables in the path have any
+ * "/" characters percent encoded.
* <p>By default this is set to {@code false} in which case the path is kept
* as a full path and expanded URI variables will preserve "/" characters.
* @param parsePath whether to parse the path into path segments
@@ -87,24 +64,55 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler {
return this.parsePath;
}
+ /**
+ * Whether to encode characters outside the unreserved set as defined in
+ * <a href="https://tools.ietf.org/html/rfc3986#section-2">RFC 3986 Section 2</a>.
+ * This ensures a URI variable value will not contain any characters with a
+ * reserved purpose.
+ * <p>By default this is set to {@code false} in which case only characters
+ * illegal for the given URI component are encoded. For example when expanding
+ * a URI variable into a path segment the "/" character is illegal and
+ * encoded. The ";" character however is legal and not encoded even though
+ * it has a reserved purpose.
+ * <p><strong>Note:</strong> this property supersedes the need to also set
+ * the {@link #setParsePath parsePath} property.
+ * @param strictEncoding whether to perform strict encoding
+ * @since 4.3
+ */
+ public void setStrictEncoding(boolean strictEncoding) {
+ this.strictEncoding = strictEncoding;
+ }
+
+ /**
+ * Whether to strictly encode any character outside the unreserved set.
+ */
+ public boolean isStrictEncoding() {
+ return this.strictEncoding;
+ }
+
@Override
- public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
+ protected URI expandInternal(String uriTemplate, Map<String, ?> uriVariables) {
UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
- UriComponents uriComponents = uriComponentsBuilder.build().expand(uriVariables).encode();
- return insertBaseUrl(uriComponents);
+ UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables);
+ return createUri(uriComponents);
}
@Override
- public URI expand(String uriTemplate, Object... uriVariableValues) {
+ protected URI expandInternal(String uriTemplate, Object... uriVariables) {
UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
- UriComponents uriComponents = uriComponentsBuilder.build().expand(uriVariableValues).encode();
- return insertBaseUrl(uriComponents);
+ UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables);
+ return createUri(uriComponents);
}
+ /**
+ * Create a {@code UriComponentsBuilder} from the URI template string.
+ * This implementation also breaks up the path into path segments depending
+ * on whether {@link #setParsePath parsePath} is enabled.
+ */
protected UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate);
- if (shouldParsePath()) {
+ if (shouldParsePath() && !isStrictEncoding()) {
List<String> pathSegments = builder.build().getPathSegments();
builder.replacePath(null);
for (String pathSegment : pathSegments) {
@@ -114,16 +122,50 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler {
return builder;
}
- protected URI insertBaseUrl(UriComponents uriComponents) {
- if (getBaseUrl() == null || uriComponents.getHost() != null) {
- return uriComponents.toUri();
+ protected UriComponents expandAndEncode(UriComponentsBuilder builder, Map<String, ?> uriVariables) {
+ if (!isStrictEncoding()) {
+ return builder.buildAndExpand(uriVariables).encode();
+ }
+ else {
+ Map<String, Object> encodedUriVars = new HashMap<String, Object>(uriVariables.size());
+ for (Map.Entry<String, ?> entry : uriVariables.entrySet()) {
+ encodedUriVars.put(entry.getKey(), applyStrictEncoding(entry.getValue()));
+ }
+ return builder.buildAndExpand(encodedUriVars);
}
- String url = getBaseUrl() + uriComponents.toUriString();
+ }
+
+ protected UriComponents expandAndEncode(UriComponentsBuilder builder, Object[] uriVariables) {
+ if (!isStrictEncoding()) {
+ return builder.buildAndExpand(uriVariables).encode();
+ }
+ else {
+ Object[] encodedUriVars = new Object[uriVariables.length];
+ for (int i = 0; i < uriVariables.length; i++) {
+ encodedUriVars[i] = applyStrictEncoding(uriVariables[i]);
+ }
+ return builder.buildAndExpand(encodedUriVars);
+ }
+ }
+
+ private String applyStrictEncoding(Object value) {
+ String stringValue = (value != null ? value.toString() : "");
+ try {
+ return UriUtils.encode(stringValue, "UTF-8");
+ }
+ catch (UnsupportedEncodingException ex) {
+ // Should never happen
+ throw new IllegalStateException("Failed to encode URI variable", ex);
+ }
+ }
+
+ private URI createUri(UriComponents uriComponents) {
try {
- return new URI(url);
+ // Avoid further encoding (in the case of strictEncoding=true)
+ return new URI(uriComponents.toUriString());
}
catch (URISyntaxException ex) {
- throw new IllegalArgumentException("Invalid URL after inserting base URL: " + url, ex);
+ throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex);
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
index e25466ec..2aceca89 100644
--- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
+++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
@@ -61,6 +61,7 @@ final class HierarchicalUriComponents extends UriComponents {
private final boolean encoded;
+
/**
* Package-private constructor. All arguments are optional, and can be {@code null}.
* @param scheme the scheme
@@ -336,6 +337,7 @@ final class HierarchicalUriComponents extends UriComponents {
private MultiValueMap<String, String> expandQueryParams(UriTemplateVariables variables) {
int size = this.queryParams.size();
MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(size);
+ variables = new QueryUriTemplateVariables(variables);
for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) {
String name = expandUriComponent(entry.getKey(), variables);
List<String> values = new ArrayList<String>(entry.getValue().size());
@@ -882,4 +884,23 @@ final class HierarchicalUriComponents extends UriComponents {
}
};
+
+ private static class QueryUriTemplateVariables implements UriTemplateVariables {
+
+ private final UriTemplateVariables delegate;
+
+ public QueryUriTemplateVariables(UriTemplateVariables delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object getValue(String name) {
+ Object value = this.delegate.getValue(name);
+ if (ObjectUtils.isArray(value)) {
+ value = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(value));
+ }
+ return value;
+ }
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java b/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java
index cb43aa9e..3546efd1 100644
--- a/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -36,7 +36,6 @@ import org.springframework.util.Assert;
* @author Martin Kersten
* @author Craig Andrews
* @since 01.03.2003
- * @see org.apache.commons.lang.StringEscapeUtils
*/
public abstract class HtmlUtils {
@@ -75,7 +74,7 @@ public abstract class HtmlUtils {
* http://www.w3.org/TR/html4/sgml/entities.html
* </a>
* @param input the (unescaped) input string
- * @param encoding The name of a supported {@link java.nio.charset.Charset charset}
+ * @param encoding the name of a supported {@link java.nio.charset.Charset charset}
* @return the escaped string
* @since 4.1.2
*/
@@ -126,7 +125,7 @@ public abstract class HtmlUtils {
* http://www.w3.org/TR/html4/sgml/entities.html
* </a>
* @param input the (unescaped) input string
- * @param encoding The name of a supported {@link java.nio.charset.Charset charset}
+ * @param encoding the name of a supported {@link java.nio.charset.Charset charset}
* @return the escaped string
* @since 4.1.2
*/
@@ -178,7 +177,7 @@ public abstract class HtmlUtils {
* http://www.w3.org/TR/html4/sgml/entities.html
* </a>
* @param input the (unescaped) input string
- * @param encoding The name of a supported {@link java.nio.charset.Charset charset}
+ * @param encoding the name of a supported {@link java.nio.charset.Charset charset}
* @return the escaped string
* @since 4.1.2
*/
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
index 43d60943..659030af 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
@@ -105,7 +105,7 @@ public class UriTemplate implements Serializable {
* <p>Example:
* <pre class="code">
* UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
- * System.out.println(template.expand("Rest & Relax", "42));
+ * System.out.println(template.expand("Rest & Relax", 42));
* </pre>
* will print: <blockquote>{@code http://example.com/hotels/Rest%20%26%20Relax/bookings/42}</blockquote>
* @param uriVariableValues the array of URI variables
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
index ac04e31e..c03eb191 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -20,15 +20,23 @@ import java.net.URI;
import java.util.Map;
/**
- * A strategy for expanding a URI template with URI variables into a {@link URI}.
+ * Strategy for expanding a URI template with full control over the URI template
+ * syntax and the encoding of variables. Also a convenient central point for
+ * pre-processing all URI templates for example to insert a common base path.
+ *
+ * <p>Supported as a property on the {@code RestTemplate} as well as the
+ * {@code AsyncRestTemplate}. The {@link DefaultUriTemplateHandler} is built
+ * on Spring's URI template support via {@link UriComponentsBuilder}. An
+ * alternative implementation may be used to plug external URI template libraries.
*
* @author Rossen Stoyanchev
* @since 4.2
+ * @see org.springframework.web.client.RestTemplate#setUriTemplateHandler
*/
public interface UriTemplateHandler {
/**
- * Expand the give URI template with a map of URI variables.
+ * Expand the given URI template from a map of URI variables.
* @param uriTemplate the URI template string
* @param uriVariables the URI variables
* @return the resulting URI
@@ -36,11 +44,11 @@ public interface UriTemplateHandler {
URI expand(String uriTemplate, Map<String, ?> uriVariables);
/**
- * Expand the give URI template with an array of URI variable values.
+ * Expand the given URI template from an array of URI variables.
* @param uriTemplate the URI template string
- * @param uriVariableValues the URI variable values
+ * @param uriVariables the URI variable values
* @return the resulting URI
*/
- URI expand(String uriTemplate, Object... uriVariableValues);
+ URI expand(String uriTemplate, Object... uriVariables);
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
index c6809297..6b2f3e50 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
* </ul>
*
* @author Arjen Poutsma
+ * @author Juergen Hoeller
* @since 3.0
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>
*/
@@ -213,4 +214,28 @@ public abstract class UriUtils {
return (changed ? new String(bos.toByteArray(), encoding) : source);
}
+ /**
+ * Extract the file extension from the given URI path.
+ * @param path the URI path (e.g. "/products/index.html")
+ * @return the extracted file extension (e.g. "html")
+ * @since 4.3.2
+ */
+ public static String extractFileExtension(String path) {
+ int end = path.indexOf('?');
+ if (end == -1) {
+ end = path.indexOf('#');
+ if (end == -1) {
+ end = path.length();
+ }
+ }
+ int begin = path.lastIndexOf('/', end) + 1;
+ int paramIndex = path.indexOf(';', begin);
+ end = (paramIndex != -1 && paramIndex < end ? paramIndex : end);
+ int extIndex = path.lastIndexOf('.', end);
+ if (extIndex != -1 && extIndex > begin) {
+ return path.substring(extIndex + 1, end);
+ }
+ return null;
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
index 1a7a4fe3..a1cd6bf6 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -179,8 +179,8 @@ public class UrlPathHelper {
String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp);
String path;
- // if the app container sanitized the servletPath, check against the sanitized version
- if (servletPath.indexOf(sanitizedPathWithinApp) != -1) {
+ // If the app container sanitized the servletPath, check against the sanitized version
+ if (servletPath.contains(sanitizedPathWithinApp)) {
path = getRemainingPath(sanitizedPathWithinApp, servletPath, false);
}
else {
@@ -485,8 +485,8 @@ public class UrlPathHelper {
* @return the updated URI string
*/
public String removeSemicolonContent(String requestUri) {
- return this.removeSemicolonContent ?
- removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri);
+ return (this.removeSemicolonContent ?
+ removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
}
private String removeSemicolonContentInternal(String requestUri) {
diff --git a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java
index 6d8062fb..13195a76 100644
--- a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -278,7 +278,6 @@ public abstract class WebUtils {
return realPath;
}
-
/**
* Determine the session id of the given request, if any.
* @param request current HTTP request
@@ -353,7 +352,9 @@ public abstract class WebUtils {
* @param clazz the class to instantiate for a new attribute
* @return the value of the session attribute, newly created if not found
* @throws IllegalArgumentException if the session attribute could not be instantiated
+ * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes
*/
+ @Deprecated
public static Object getOrCreateSessionAttribute(HttpSession session, String name, Class<?> clazz)
throws IllegalArgumentException {
@@ -527,7 +528,9 @@ public abstract class WebUtils {
* and the values as corresponding attribute values. Keys need to be Strings.
* @param request current HTTP request
* @param attributes the attributes Map
+ * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes
*/
+ @Deprecated
public static void exposeRequestAttributes(ServletRequest request, Map<String, ?> attributes) {
Assert.notNull(request, "Request must not be null");
Assert.notNull(attributes, "Attributes Map must not be null");
@@ -689,7 +692,9 @@ public abstract class WebUtils {
* @param currentPage the current page, to be returned as fallback
* if no target page specified
* @return the page specified in the request, or current page if not found
+ * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes
*/
+ @Deprecated
public static int getTargetPage(ServletRequest request, String paramPrefix, int currentPage) {
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
@@ -713,7 +718,9 @@ public abstract class WebUtils {
* Correctly resolves nested paths such as "/products/view.html" as well.
* @param urlPath the request URL path (e.g. "/index.html")
* @return the extracted URI filename (e.g. "index")
+ * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes
*/
+ @Deprecated
public static String extractFilenameFromUrlPath(String urlPath) {
String filename = extractFullFilenameFromUrlPath(urlPath);
int dotIndex = filename.lastIndexOf('.');
@@ -729,7 +736,10 @@ public abstract class WebUtils {
* "/products/view.html" and remove any path and or query parameters.
* @param urlPath the request URL path (e.g. "/products/index.html")
* @return the extracted URI filename (e.g. "index.html")
+ * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes
+ * (or {@link UriUtils#extractFileExtension} for the file extension use case)
*/
+ @Deprecated
public static String extractFullFilenameFromUrlPath(String urlPath) {
int end = urlPath.indexOf('?');
if (end == -1) {
diff --git a/spring-web/src/test/java/org/springframework/http/CacheControlTests.java b/spring-web/src/test/java/org/springframework/http/CacheControlTests.java
index 43de2c0b..2111d2f1 100644
--- a/spring-web/src/test/java/org/springframework/http/CacheControlTests.java
+++ b/spring-web/src/test/java/org/springframework/http/CacheControlTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -63,4 +63,17 @@ public class CacheControlTests {
CacheControl cc = CacheControl.noStore();
assertThat(cc.getHeaderValue(), Matchers.equalTo("no-store"));
}
+
+ @Test
+ public void staleIfError() throws Exception {
+ CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).staleIfError(2, TimeUnit.HOURS);
+ assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, stale-if-error=7200"));
+ }
+
+ @Test
+ public void staleWhileRevalidate() throws Exception {
+ CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).staleWhileRevalidate(2, TimeUnit.HOURS);
+ assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, stale-while-revalidate=7200"));
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java
index 9fa07041..77c79da4 100644
--- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java
+++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java
@@ -32,6 +32,7 @@ import java.util.TimeZone;
import org.hamcrest.Matchers;
import org.junit.Test;
+import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
@@ -39,6 +40,7 @@ import static org.junit.Assert.*;
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
+ * @author Brian Clozel
*/
public class HttpHeadersTests {
@@ -46,6 +48,13 @@ public class HttpHeadersTests {
@Test
+ public void getFirst() {
+ headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public");
+ headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000");
+ assertThat(headers.getFirst(HttpHeaders.CACHE_CONTROL), is("max-age=1000, public"));
+ }
+
+ @Test
public void accept() {
MediaType mediaType1 = new MediaType("text", "html");
MediaType mediaType2 = new MediaType("text", "plain");
@@ -58,13 +67,22 @@ public class HttpHeadersTests {
}
@Test // SPR-9655
- public void acceptIPlanet() {
+ public void acceptWithMultipleHeaderValues() {
headers.add("Accept", "text/html");
headers.add("Accept", "text/plain");
List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "plain"));
assertEquals("Invalid Accept header", expected, headers.getAccept());
}
+ @Test // SPR-14506
+ public void acceptWithMultipleCommaSeparatedHeaderValues() {
+ headers.add("Accept", "text/html,text/pdf");
+ headers.add("Accept", "text/plain,text/csv");
+ List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "pdf"),
+ new MediaType("text", "plain"), new MediaType("text", "csv"));
+ assertEquals("Invalid Accept header", expected, headers.getAccept());
+ }
+
@Test
public void acceptCharsets() {
Charset charset1 = Charset.forName("UTF-8");
@@ -133,6 +151,29 @@ public class HttpHeadersTests {
}
@Test
+ public void ifMatch() {
+ String ifMatch = "\"v2.6\"";
+ headers.setIfMatch(ifMatch);
+ assertEquals("Invalid If-Match header", ifMatch, headers.getIfMatch().get(0));
+ assertEquals("Invalid If-Match header", "\"v2.6\"", headers.getFirst("If-Match"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void ifMatchIllegalHeader() {
+ headers.setIfMatch("Illegal");
+ headers.getIfMatch();
+ }
+
+ @Test
+ public void ifMatchMultipleHeaders() {
+ headers.add(HttpHeaders.IF_MATCH, "\"v2,0\"");
+ headers.add(HttpHeaders.IF_MATCH, "W/\"v2,1\", \"v2,2\"");
+ assertEquals("Invalid If-Match header", "\"v2,0\"", headers.get(HttpHeaders.IF_MATCH).get(0));
+ assertEquals("Invalid If-Match header", "W/\"v2,1\", \"v2,2\"", headers.get(HttpHeaders.IF_MATCH).get(1));
+ assertThat(headers.getIfMatch(), Matchers.contains("\"v2,0\"", "W/\"v2,1\"", "\"v2,2\""));
+ }
+
+ @Test
public void ifNoneMatch() {
String ifNoneMatch = "\"v2.6\"";
headers.setIfNoneMatch(ifNoneMatch);
@@ -141,15 +182,23 @@ public class HttpHeadersTests {
}
@Test
+ public void ifNoneMatchWildCard() {
+ String ifNoneMatch = "*";
+ headers.setIfNoneMatch(ifNoneMatch);
+ assertEquals("Invalid If-None-Match header", ifNoneMatch, headers.getIfNoneMatch().get(0));
+ assertEquals("Invalid If-None-Match header", "*", headers.getFirst("If-None-Match"));
+ }
+
+ @Test
public void ifNoneMatchList() {
String ifNoneMatch1 = "\"v2.6\"";
- String ifNoneMatch2 = "\"v2.7\"";
+ String ifNoneMatch2 = "\"v2.7\", \"v2.8\"";
List<String> ifNoneMatchList = new ArrayList<String>(2);
ifNoneMatchList.add(ifNoneMatch1);
ifNoneMatchList.add(ifNoneMatch2);
headers.setIfNoneMatch(ifNoneMatchList);
- assertEquals("Invalid If-None-Match header", ifNoneMatchList, headers.getIfNoneMatch());
- assertEquals("Invalid If-None-Match header", "\"v2.6\", \"v2.7\"", headers.getFirst("If-None-Match"));
+ assertThat(headers.getIfNoneMatch(), Matchers.contains("\"v2.6\"", "\"v2.7\"", "\"v2.8\""));
+ assertEquals("Invalid If-None-Match header", "\"v2.6\", \"v2.7\", \"v2.8\"", headers.getFirst("If-None-Match"));
}
@Test
@@ -256,6 +305,13 @@ public class HttpHeadersTests {
}
@Test
+ public void cacheControlAllValues() {
+ headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public");
+ headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000");
+ assertThat(headers.getCacheControl(), is("max-age=1000, public, s-maxage=1000"));
+ }
+
+ @Test
public void contentDisposition() {
headers.setContentDispositionFormData("name", null);
assertEquals("Invalid Content-Disposition header", "form-data; name=\"name\"",
@@ -291,6 +347,16 @@ public class HttpHeadersTests {
}
@Test
+ public void accessControlAllowHeadersMultipleValues() {
+ List<String> allowedHeaders = headers.getAccessControlAllowHeaders();
+ assertThat(allowedHeaders, Matchers.emptyCollectionOf(String.class));
+ headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "header1, header2");
+ headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "header3");
+ allowedHeaders = headers.getAccessControlAllowHeaders();
+ assertEquals(Arrays.asList("header1", "header2", "header3"), allowedHeaders);
+ }
+
+ @Test
public void accessControlAllowMethods() {
List<HttpMethod> allowedMethods = headers.getAccessControlAllowMethods();
assertThat(allowedMethods, Matchers.emptyCollectionOf(HttpMethod.class));
diff --git a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java
index 85870b3c..5ddec37a 100644
--- a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java
+++ b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -13,19 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.http;
+import java.io.IOException;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.support.ResourceRegion;
+
import static org.junit.Assert.*;
+import static org.mockito.BDDMockito.*;
+import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link HttpRange}.
*
* @author Rossen Stoyanchev
+ * @author Brian Clozel
*/
public class HttpRangeTests {
@@ -100,4 +110,39 @@ public class HttpRangeTests {
assertEquals("Invalid Range header", "bytes=0-499, 9500-, -500", HttpRange.toString(ranges));
}
+ @Test
+ public void toResourceRegion() {
+ byte[] bytes = "Spring Framework".getBytes(Charset.forName("UTF-8"));
+ ByteArrayResource resource = new ByteArrayResource(bytes);
+ HttpRange range = HttpRange.createByteRange(0, 5);
+ ResourceRegion region = range.toResourceRegion(resource);
+ assertEquals(resource, region.getResource());
+ assertEquals(0L, region.getPosition());
+ assertEquals(6L, region.getCount());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void toResourceRegionInputStreamResource() {
+ InputStreamResource resource = mock(InputStreamResource.class);
+ HttpRange range = HttpRange.createByteRange(0, 9);
+ range.toResourceRegion(resource);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void toResourceRegionIllegalLength() {
+ ByteArrayResource resource = mock(ByteArrayResource.class);
+ given(resource.contentLength()).willReturn(-1L);
+ HttpRange range = HttpRange.createByteRange(0, 9);
+ range.toResourceRegion(resource);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ @SuppressWarnings("unchecked")
+ public void toResourceRegionExceptionLength() {
+ ByteArrayResource resource = mock(ByteArrayResource.class);
+ given(resource.contentLength()).willThrow(IOException.class);
+ HttpRange range = HttpRange.createByteRange(0, 9);
+ range.toResourceRegion(resource);
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java b/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java
index 574f02bd..3624ece7 100644
--- a/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java
+++ b/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -85,6 +85,7 @@ public class HttpStatusTests {
statusCodes.put(428, "PRECONDITION_REQUIRED");
statusCodes.put(429, "TOO_MANY_REQUESTS");
statusCodes.put(431, "REQUEST_HEADER_FIELDS_TOO_LARGE");
+ statusCodes.put(451, "UNAVAILABLE_FOR_LEGAL_REASONS");
statusCodes.put(500, "INTERNAL_SERVER_ERROR");
statusCodes.put(501, "NOT_IMPLEMENTED");
diff --git a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java
index 99665b93..ca31a975 100644
--- a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java
+++ b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -138,7 +138,7 @@ public class MediaTypeTests {
assertNotNull("No media types returned", mediaTypes);
assertEquals("Invalid amount of media types", 4, mediaTypes.size());
- mediaTypes = MediaType.parseMediaTypes(null);
+ mediaTypes = MediaType.parseMediaTypes("");
assertNotNull("No media types returned", mediaTypes);
assertEquals("Invalid amount of media types", 0, mediaTypes.size());
}
diff --git a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java
index de235366..9b8268d5 100644
--- a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java
+++ b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -19,11 +19,14 @@ package org.springframework.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.junit.Test;
+import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.util.UriTemplate;
import static org.junit.Assert.*;
@@ -148,4 +151,14 @@ public class RequestEntityTests {
}
+ @Test // SPR-13154
+ public void types() throws URISyntaxException {
+ URI url = new URI("http://example.com");
+ List<String> body = Arrays.asList("foo", "bar");
+ ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<String>>() {};
+
+ RequestEntity<?> entity = RequestEntity.post(url).body(body, typeReference.getType());
+ assertEquals(typeReference.getType(), entity.getType());
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java b/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java
index dca351a4..5bc9d3f7 100644
--- a/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java
+++ b/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -252,4 +252,22 @@ public class ResponseEntityTests {
assertThat(cacheControlHeader, Matchers.equalTo("no-store"));
}
+ @Test
+ public void statusCodeAsInt() {
+ Integer entity = new Integer(42);
+ ResponseEntity<Integer> responseEntity = ResponseEntity.status(200).body(entity);
+
+ assertEquals(200, responseEntity.getStatusCode().value());
+ assertEquals(entity, responseEntity.getBody());
+ }
+
+ @Test
+ public void customStatusCode() {
+ Integer entity = new Integer(42);
+ ResponseEntity<Integer> responseEntity = ResponseEntity.status(299).body(entity);
+
+ assertEquals(299, responseEntity.getStatusCodeValue());
+ assertEquals(entity, responseEntity.getBody());
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java
index 8b70e32f..1f97af02 100644
--- a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java
+++ b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,16 +16,19 @@
package org.springframework.http.client;
-import static org.junit.Assert.assertEquals;
-
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import org.junit.Test;
+
import org.springframework.http.HttpMethod;
+import static org.junit.Assert.*;
+
public class BufferedSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase {
@Override
@@ -89,5 +92,11 @@ public class BufferedSimpleHttpRequestFactoryTests extends AbstractHttpRequestFa
public boolean usingProxy() {
return false;
}
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new ByteArrayInputStream(new byte[0]);
+ }
}
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java
new file mode 100644
index 00000000..12e857e6
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpMethod;
+
+/**
+ * @author Roy Clarkson
+ */
+public class OkHttp3AsyncClientHttpRequestFactoryTests extends AbstractAsyncHttpRequestFactoryTestCase {
+
+ @Override
+ protected AsyncClientHttpRequestFactory createRequestFactory() {
+ return new OkHttp3ClientHttpRequestFactory();
+ }
+
+ @Override
+ @Test
+ public void httpMethods() throws Exception {
+ super.httpMethods();
+ assertHttpMethod("patch", HttpMethod.PATCH);
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java
new file mode 100644
index 00000000..dd1874ad
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpMethod;
+
+/**
+ * @author Roy Clarkson
+ */
+public class OkHttp3ClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase {
+
+ @Override
+ protected ClientHttpRequestFactory createRequestFactory() {
+ return new OkHttp3ClientHttpRequestFactory();
+ }
+
+ @Override
+ @Test
+ public void httpMethods() throws Exception {
+ super.httpMethods();
+ assertHttpMethod("patch", HttpMethod.PATCH);
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java
index f7c367e6..f508027d 100644
--- a/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java
+++ b/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java
@@ -24,7 +24,7 @@ import org.springframework.http.HttpMethod;
* @author Luciano Leggieri
*/
public class OkHttpAsyncClientHttpRequestFactoryTests extends AbstractAsyncHttpRequestFactoryTestCase {
-
+
@Override
protected AsyncClientHttpRequestFactory createRequestFactory() {
return new OkHttpClientHttpRequestFactory();
diff --git a/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java
new file mode 100644
index 00000000..4be26a26
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2002-2016 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.client;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.BDDMockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.nio.charset.Charset;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.util.StreamUtils;
+
+/**
+ * @author Brian Clozel
+ */
+public class SimpleClientHttpResponseTests {
+
+ private final Charset UTF8 = Charset.forName("UTF-8");
+
+ private SimpleClientHttpResponse response;
+
+ private HttpURLConnection connection;
+
+ @Before
+ public void setup() throws Exception {
+ this.connection = mock(HttpURLConnection.class);
+ this.response = new SimpleClientHttpResponse(this.connection);
+ }
+
+ // SPR-14040
+ @Test
+ public void shouldNotCloseConnectionWhenResponseClosed() throws Exception {
+ TestByteArrayInputStream is = new TestByteArrayInputStream("Spring".getBytes(UTF8));
+ given(this.connection.getErrorStream()).willReturn(null);
+ given(this.connection.getInputStream()).willReturn(is);
+
+ InputStream responseStream = this.response.getBody();
+ assertThat(StreamUtils.copyToString(responseStream, UTF8), is("Spring"));
+
+ this.response.close();
+ assertTrue(is.isClosed());
+ verify(this.connection, never()).disconnect();
+ }
+
+ // SPR-14040
+ @Test
+ public void shouldDrainStreamWhenResponseClosed() throws Exception {
+ byte[] buf = new byte[6];
+ TestByteArrayInputStream is = new TestByteArrayInputStream("SpringSpring".getBytes(UTF8));
+ given(this.connection.getErrorStream()).willReturn(null);
+ given(this.connection.getInputStream()).willReturn(is);
+
+ InputStream responseStream = this.response.getBody();
+ responseStream.read(buf);
+ assertThat(new String(buf, UTF8), is("Spring"));
+ assertThat(is.available(), is(6));
+
+ this.response.close();
+ assertThat(is.available(), is(0));
+ assertTrue(is.isClosed());
+ verify(this.connection, never()).disconnect();
+ }
+
+ // SPR-14040
+ @Test
+ public void shouldDrainErrorStreamWhenResponseClosed() throws Exception {
+ byte[] buf = new byte[6];
+ TestByteArrayInputStream is = new TestByteArrayInputStream("SpringSpring".getBytes(UTF8));
+ given(this.connection.getErrorStream()).willReturn(is);
+
+ InputStream responseStream = this.response.getBody();
+ responseStream.read(buf);
+ assertThat(new String(buf, UTF8), is("Spring"));
+ assertThat(is.available(), is(6));
+
+ this.response.close();
+ assertThat(is.available(), is(0));
+ assertTrue(is.isClosed());
+ verify(this.connection, never()).disconnect();
+ }
+
+
+ class TestByteArrayInputStream extends ByteArrayInputStream {
+
+ private boolean closed;
+
+ public TestByteArrayInputStream(byte[] buf) {
+ super(buf);
+ this.closed = false;
+ }
+
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ this.closed = true;
+ }
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java b/spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java
new file mode 100644
index 00000000..ce96460d
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2002-2016 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.client.support;
+
+import java.net.URI;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.springframework.beans.DirectFieldAccessor;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests for {@link BasicAuthorizationInterceptor}.
+ *
+ * @author Phillip Webb
+ * @author Stephane Nicoll
+ */
+public class BasicAuthorizationInterceptorTests {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void createWhenUsernameIsNullShouldThrowException() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Username must not be empty");
+ new BasicAuthorizationInterceptor(null, "password");
+ }
+
+ @Test
+ public void createWhenUsernameIsEmptyShouldThrowException() throws Exception {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Username must not be empty");
+ new BasicAuthorizationInterceptor("", "password");
+ }
+
+ @Test
+ public void createWhenPasswordIsNullShouldUseEmptyPassword() throws Exception {
+ BasicAuthorizationInterceptor interceptor = new BasicAuthorizationInterceptor(
+ "username", null);
+ assertEquals("", new DirectFieldAccessor(interceptor).getPropertyValue("password"));
+ }
+
+ @Test
+ public void interceptShouldAddHeader() throws Exception {
+ SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET);
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ byte[] body = new byte[] {};
+ new BasicAuthorizationInterceptor("spring", "boot").intercept(request, body,
+ execution);
+ verify(execution).execute(request, body);
+ assertEquals("Basic c3ByaW5nOmJvb3Q=", request.getHeaders().getFirst("Authorization"));
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java
index 488e1826..aaa03b59 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java
@@ -261,7 +261,7 @@ public class FormHttpMessageConverterTests {
@Override
public String getCharacterEncoding() {
MediaType type = this.outputMessage.getHeaders().getContentType();
- return (type != null && type.getCharSet() != null ? type.getCharSet().name() : null);
+ return (type != null && type.getCharset() != null ? type.getCharset().name() : null);
}
@Override
diff --git a/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java
index 10752f22..d3a3d96f 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,6 +16,15 @@
package org.springframework.http.converter;
+import static org.hamcrest.core.Is.*;
+import static org.hamcrest.core.IsInstanceOf.*;
+import static org.junit.Assert.*;
+import static org.mockito.BDDMockito.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
@@ -31,13 +40,10 @@ import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
import org.springframework.util.FileCopyUtils;
-import static org.hamcrest.core.Is.*;
-import static org.hamcrest.core.IsInstanceOf.*;
-import static org.junit.Assert.*;
-
/**
* @author Arjen Poutsma
* @author Kazuki Shimizu
+ * @author Brian Clozel
*/
public class ResourceHttpMessageConverterTests {
@@ -45,18 +51,18 @@ public class ResourceHttpMessageConverterTests {
@Test
- public void canRead() {
+ public void canReadResource() {
assertTrue(converter.canRead(Resource.class, new MediaType("application", "octet-stream")));
}
@Test
- public void canWrite() {
+ public void canWriteResource() {
assertTrue(converter.canWrite(Resource.class, new MediaType("application", "octet-stream")));
assertTrue(converter.canWrite(Resource.class, MediaType.ALL));
}
@Test
- public void read() throws IOException {
+ public void shouldReadImageResource() throws IOException {
byte[] body = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("logo.jpg"));
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG);
@@ -65,7 +71,7 @@ public class ResourceHttpMessageConverterTests {
}
@Test // SPR-13443
- public void readWithInputStreamResource() throws IOException {
+ public void shouldReadInputStreamResource() throws IOException {
try (InputStream body = getClass().getResourceAsStream("logo.jpg") ) {
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG);
@@ -76,7 +82,7 @@ public class ResourceHttpMessageConverterTests {
}
@Test
- public void write() throws IOException {
+ public void shouldWriteImageResource() throws IOException {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
Resource body = new ClassPathResource("logo.jpg", getClass());
converter.write(body, null, outputMessage);
@@ -94,4 +100,45 @@ public class ResourceHttpMessageConverterTests {
assertTrue(Arrays.equals(byteArray, outputMessage.getBodyAsBytes()));
}
+ // SPR-12999
+ @Test @SuppressWarnings("unchecked")
+ public void writeContentNotGettingInputStream() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Resource resource = mock(Resource.class);
+ given(resource.getInputStream()).willThrow(FileNotFoundException.class);
+
+ converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage);
+
+ assertEquals(0, outputMessage.getHeaders().getContentLength());
+ }
+
+ // SPR-12999
+ @Test
+ public void writeContentNotClosingInputStream() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Resource resource = mock(Resource.class);
+ InputStream inputStream = mock(InputStream.class);
+ given(resource.getInputStream()).willReturn(inputStream);
+ given(inputStream.read(any())).willReturn(-1);
+ doThrow(new NullPointerException()).when(inputStream).close();
+
+ converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage);
+
+ assertEquals(0, outputMessage.getHeaders().getContentLength());
+ }
+
+ // SPR-13620
+ @Test @SuppressWarnings("unchecked")
+ public void writeContentInputStreamThrowingNullPointerException() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Resource resource = mock(Resource.class);
+ InputStream in = mock(InputStream.class);
+ given(resource.getInputStream()).willReturn(in);
+ given(in.read(any())).willThrow(NullPointerException.class);
+
+ converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage);
+
+ assertEquals(0, outputMessage.getHeaders().getContentLength());
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java
new file mode 100644
index 00000000..a4f9c465
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2002-2016 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.converter;
+
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.ResourceRegion;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRange;
+import org.springframework.http.MediaType;
+import org.springframework.http.MockHttpOutputMessage;
+import org.springframework.util.StringUtils;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+/**
+ * Test cases for {@link ResourceRegionHttpMessageConverter} class.
+ *
+ * @author Brian Clozel
+ */
+public class ResourceRegionHttpMessageConverterTests {
+
+ private final ResourceRegionHttpMessageConverter converter = new ResourceRegionHttpMessageConverter();
+
+ @Test
+ public void canReadResource() {
+ assertFalse(converter.canRead(Resource.class, MediaType.APPLICATION_OCTET_STREAM));
+ assertFalse(converter.canRead(Resource.class, MediaType.ALL));
+ assertFalse(converter.canRead(List.class, MediaType.APPLICATION_OCTET_STREAM));
+ assertFalse(converter.canRead(List.class, MediaType.ALL));
+ }
+
+ @Test
+ public void canWriteResource() {
+ assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.APPLICATION_OCTET_STREAM));
+ assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.ALL));
+ }
+
+ @Test
+ public void canWriteResourceCollection() {
+ Type resourceRegionList = new ParameterizedTypeReference<List<ResourceRegion>>() {}.getType();
+ assertTrue(converter.canWrite(resourceRegionList, null, MediaType.APPLICATION_OCTET_STREAM));
+ assertTrue(converter.canWrite(resourceRegionList, null, MediaType.ALL));
+
+ assertFalse(converter.canWrite(List.class, MediaType.APPLICATION_OCTET_STREAM));
+ assertFalse(converter.canWrite(List.class, MediaType.ALL));
+ }
+
+ @Test
+ public void shouldWritePartialContentByteRange() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Resource body = new ClassPathResource("byterangeresource.txt", getClass());
+ ResourceRegion region = HttpRange.createByteRange(0, 5).toResourceRegion(body);
+ converter.write(region, MediaType.TEXT_PLAIN, outputMessage);
+
+ HttpHeaders headers = outputMessage.getHeaders();
+ assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN));
+ assertThat(headers.getContentLength(), is(6L));
+ assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1));
+ assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 0-5/39"));
+ assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Spring"));
+ }
+
+ @Test
+ public void shouldWritePartialContentByteRangeNoEnd() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Resource body = new ClassPathResource("byterangeresource.txt", getClass());
+ ResourceRegion region = HttpRange.createByteRange(7).toResourceRegion(body);
+ converter.write(region, MediaType.TEXT_PLAIN, outputMessage);
+
+ HttpHeaders headers = outputMessage.getHeaders();
+ assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN));
+ assertThat(headers.getContentLength(), is(32L));
+ assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1));
+ assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 7-38/39"));
+ assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Framework test resource content."));
+ }
+
+ @Test
+ public void partialContentMultipleByteRanges() throws Exception {
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ Resource body = new ClassPathResource("byterangeresource.txt", getClass());
+ List<HttpRange> rangeList = HttpRange.parseRanges("bytes=0-5,7-15,17-20,22-38");
+ List<ResourceRegion> regions = new ArrayList<ResourceRegion>();
+ for(HttpRange range : rangeList) {
+ regions.add(range.toResourceRegion(body));
+ }
+
+ converter.write(regions, MediaType.TEXT_PLAIN, outputMessage);
+
+ HttpHeaders headers = outputMessage.getHeaders();
+ assertThat(headers.getContentType().toString(), Matchers.startsWith("multipart/byteranges;boundary="));
+ String boundary = "--" + headers.getContentType().toString().substring(30);
+ String content = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
+ String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true);
+
+ assertThat(ranges[0], is(boundary));
+ assertThat(ranges[1], is("Content-Type: text/plain"));
+ assertThat(ranges[2], is("Content-Range: bytes 0-5/39"));
+ assertThat(ranges[3], is("Spring"));
+
+ assertThat(ranges[4], is(boundary));
+ assertThat(ranges[5], is("Content-Type: text/plain"));
+ assertThat(ranges[6], is("Content-Range: bytes 7-15/39"));
+ assertThat(ranges[7], is("Framework"));
+
+ assertThat(ranges[8], is(boundary));
+ assertThat(ranges[9], is("Content-Type: text/plain"));
+ assertThat(ranges[10], is("Content-Range: bytes 17-20/39"));
+ assertThat(ranges[11], is("test"));
+
+ assertThat(ranges[12], is(boundary));
+ assertThat(ranges[13], is("Content-Type: text/plain"));
+ assertThat(ranges[14], is("Content-Range: bytes 22-38/39"));
+ assertThat(ranges[15], is("resource content."));
+ }
+
+} \ No newline at end of file
diff --git a/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java
index 6dcdb25f..2fa2a574 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -22,86 +22,97 @@ import java.nio.charset.Charset;
import org.junit.Before;
import org.junit.Test;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
import static org.junit.Assert.*;
-/** @author Arjen Poutsma */
+/**
+ * @author Arjen Poutsma
+ * @author Rossen Stoyanchev
+ */
public class StringHttpMessageConverterTests {
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ public static final MediaType TEXT_PLAIN_UTF_8 = new MediaType("text", "plain", UTF_8);
+
+
private StringHttpMessageConverter converter;
+ private MockHttpOutputMessage outputMessage;
+
+
@Before
public void setUp() {
- converter = new StringHttpMessageConverter();
+ this.converter = new StringHttpMessageConverter();
+ this.outputMessage = new MockHttpOutputMessage();
}
+
@Test
public void canRead() {
- assertTrue(converter.canRead(String.class, new MediaType("text", "plain")));
+ assertTrue(this.converter.canRead(String.class, MediaType.TEXT_PLAIN));
}
@Test
public void canWrite() {
- assertTrue(converter.canWrite(String.class, new MediaType("text", "plain")));
- assertTrue(converter.canWrite(String.class, MediaType.ALL));
+ assertTrue(this.converter.canWrite(String.class, MediaType.TEXT_PLAIN));
+ assertTrue(this.converter.canWrite(String.class, MediaType.ALL));
}
@Test
public void read() throws IOException {
String body = "Hello World";
- Charset charset = Charset.forName("UTF-8");
- MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset));
- inputMessage.getHeaders().setContentType(new MediaType("text", "plain", charset));
- String result = converter.read(String.class, inputMessage);
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(UTF_8));
+ inputMessage.getHeaders().setContentType(TEXT_PLAIN_UTF_8);
+ String result = this.converter.read(String.class, inputMessage);
+
assertEquals("Invalid result", body, result);
}
@Test
public void writeDefaultCharset() throws IOException {
Charset iso88591 = Charset.forName("ISO-8859-1");
- MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
String body = "H\u00e9llo W\u00f6rld";
- converter.write(body, null, outputMessage);
- assertEquals("Invalid result", body, outputMessage.getBodyAsString(iso88591));
- assertEquals("Invalid content-type", new MediaType("text", "plain", iso88591),
- outputMessage.getHeaders().getContentType());
- assertEquals("Invalid content-length", body.getBytes(iso88591).length,
- outputMessage.getHeaders().getContentLength());
- assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty());
+ this.converter.write(body, null, this.outputMessage);
+
+ HttpHeaders headers = this.outputMessage.getHeaders();
+ assertEquals(body, this.outputMessage.getBodyAsString(iso88591));
+ assertEquals(new MediaType("text", "plain", iso88591), headers.getContentType());
+ assertEquals(body.getBytes(iso88591).length, headers.getContentLength());
+ assertFalse(headers.getAcceptCharset().isEmpty());
}
@Test
public void writeUTF8() throws IOException {
- Charset utf8 = Charset.forName("UTF-8");
- MediaType contentType = new MediaType("text", "plain", utf8);
- MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
String body = "H\u00e9llo W\u00f6rld";
- converter.write(body, contentType, outputMessage);
- assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8));
- assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType());
- assertEquals("Invalid content-length", body.getBytes(utf8).length,
- outputMessage.getHeaders().getContentLength());
- assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty());
+ this.converter.write(body, TEXT_PLAIN_UTF_8, this.outputMessage);
+
+ HttpHeaders headers = this.outputMessage.getHeaders();
+ assertEquals(body, this.outputMessage.getBodyAsString(UTF_8));
+ assertEquals(TEXT_PLAIN_UTF_8, headers.getContentType());
+ assertEquals(body.getBytes(UTF_8).length, headers.getContentLength());
+ assertFalse(headers.getAcceptCharset().isEmpty());
}
// SPR-8867
@Test
public void writeOverrideRequestedContentType() throws IOException {
- Charset utf8 = Charset.forName("UTF-8");
- MediaType requestedContentType = new MediaType("text", "html");
- MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
- MediaType contentType = new MediaType("text", "plain", utf8);
- outputMessage.getHeaders().setContentType(contentType);
String body = "H\u00e9llo W\u00f6rld";
- converter.write(body, requestedContentType, outputMessage);
- assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8));
- assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType());
- assertEquals("Invalid content-length", body.getBytes(utf8).length,
- outputMessage.getHeaders().getContentLength());
- assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty());
+ MediaType requestedContentType = new MediaType("text", "html");
+
+ HttpHeaders headers = this.outputMessage.getHeaders();
+ headers.setContentType(TEXT_PLAIN_UTF_8);
+ this.converter.write(body, requestedContentType, this.outputMessage);
+
+ assertEquals(body, this.outputMessage.getBodyAsString(UTF_8));
+ assertEquals(TEXT_PLAIN_UTF_8, headers.getContentType());
+ assertEquals(body.getBytes(UTF_8).length, headers.getContentLength());
+ assertFalse(headers.getAcceptCharset().isEmpty());
}
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java
index bcaa76b9..54d83074 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -42,13 +42,13 @@ import static org.junit.Assert.assertTrue;
*/
public class AtomFeedHttpMessageConverterTests {
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
private AtomFeedHttpMessageConverter converter;
- private Charset utf8;
@Before
public void setUp() {
- utf8 = Charset.forName("UTF-8");
converter = new AtomFeedHttpMessageConverter();
XMLUnit.setIgnoreWhitespace(true);
}
@@ -56,20 +56,20 @@ public class AtomFeedHttpMessageConverterTests {
@Test
public void canRead() {
assertTrue(converter.canRead(Feed.class, new MediaType("application", "atom+xml")));
- assertTrue(converter.canRead(Feed.class, new MediaType("application", "atom+xml", utf8)));
+ assertTrue(converter.canRead(Feed.class, new MediaType("application", "atom+xml", UTF_8)));
}
@Test
public void canWrite() {
assertTrue(converter.canWrite(Feed.class, new MediaType("application", "atom+xml")));
- assertTrue(converter.canWrite(Feed.class, new MediaType("application", "atom+xml", Charset.forName("UTF-8"))));
+ assertTrue(converter.canWrite(Feed.class, new MediaType("application", "atom+xml", UTF_8)));
}
@Test
public void read() throws IOException {
InputStream is = getClass().getResourceAsStream("atom.xml");
MockHttpInputMessage inputMessage = new MockHttpInputMessage(is);
- inputMessage.getHeaders().setContentType(new MediaType("application", "atom+xml", utf8));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "atom+xml", UTF_8));
Feed result = converter.read(Feed.class, inputMessage);
assertEquals("title", result.getTitle());
assertEquals("subtitle", result.getSubtitle().getValue());
@@ -106,12 +106,12 @@ public class AtomFeedHttpMessageConverterTests {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(feed, null, outputMessage);
- assertEquals("Invalid content-type", new MediaType("application", "atom+xml", utf8),
+ assertEquals("Invalid content-type", new MediaType("application", "atom+xml", UTF_8),
outputMessage.getHeaders().getContentType());
String expected = "<feed xmlns=\"http://www.w3.org/2005/Atom\">" + "<title>title</title>" +
"<entry><id>id1</id><title>title1</title></entry>" +
"<entry><id>id2</id><title>title2</title></entry></feed>";
- assertXMLEqual(expected, outputMessage.getBodyAsString(utf8));
+ assertXMLEqual(expected, outputMessage.getBodyAsString(UTF_8));
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java
index 16fcfc00..a8ceeb3b 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -42,34 +42,35 @@ import static org.junit.Assert.assertTrue;
*/
public class RssChannelHttpMessageConverterTests {
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
private RssChannelHttpMessageConverter converter;
- private Charset utf8;
@Before
public void setUp() {
- utf8 = Charset.forName("UTF-8");
converter = new RssChannelHttpMessageConverter();
XMLUnit.setIgnoreWhitespace(true);
}
+
@Test
public void canRead() {
assertTrue(converter.canRead(Channel.class, new MediaType("application", "rss+xml")));
- assertTrue(converter.canRead(Channel.class, new MediaType("application", "rss+xml", utf8)));
+ assertTrue(converter.canRead(Channel.class, new MediaType("application", "rss+xml", UTF_8)));
}
@Test
public void canWrite() {
assertTrue(converter.canWrite(Channel.class, new MediaType("application", "rss+xml")));
- assertTrue(converter.canWrite(Channel.class, new MediaType("application", "rss+xml", Charset.forName("UTF-8"))));
+ assertTrue(converter.canWrite(Channel.class, new MediaType("application", "rss+xml", UTF_8)));
}
@Test
public void read() throws IOException {
InputStream is = getClass().getResourceAsStream("rss.xml");
MockHttpInputMessage inputMessage = new MockHttpInputMessage(is);
- inputMessage.getHeaders().setContentType(new MediaType("application", "rss+xml", utf8));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "rss+xml", UTF_8));
Channel result = converter.read(Channel.class, inputMessage);
assertEquals("title", result.getTitle());
assertEquals("http://example.com", result.getLink());
@@ -106,14 +107,14 @@ public class RssChannelHttpMessageConverterTests {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(channel, null, outputMessage);
- assertEquals("Invalid content-type", new MediaType("application", "rss+xml", utf8),
+ assertEquals("Invalid content-type", new MediaType("application", "rss+xml", UTF_8),
outputMessage.getHeaders().getContentType());
String expected = "<rss version=\"2.0\">" +
"<channel><title>title</title><link>http://example.com</link><description>description</description>" +
"<item><title>title1</title></item>" +
"<item><title>title2</title></item>" +
"</channel></rss>";
- assertXMLEqual(expected, outputMessage.getBodyAsString(utf8));
+ assertXMLEqual(expected, outputMessage.getBodyAsString(UTF_8));
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java
index 0f02ad21..ddb9e1e6 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -22,9 +22,11 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@@ -62,7 +64,7 @@ import com.fasterxml.jackson.databind.ser.std.ClassSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import com.fasterxml.jackson.databind.type.SimpleType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
-
+import kotlin.ranges.IntRange;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.Test;
@@ -77,6 +79,7 @@ import static org.junit.Assert.*;
*
* @author Sebastien Deleuze
*/
+@SuppressWarnings("deprecation")
public class Jackson2ObjectMapperBuilderTests {
private static final String DATE_FORMAT = "yyyy-MM-dd";
@@ -249,13 +252,17 @@ public class Jackson2ObjectMapperBuilderTests {
assertEquals(timestamp.toString(), new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8"));
Path file = Paths.get("foo");
- assertEquals("\"foo\"", new String(objectMapper.writeValueAsBytes(file), "UTF-8"));
+ assertTrue(new String(objectMapper.writeValueAsBytes(file), "UTF-8").endsWith("foo\""));
Optional<String> optional = Optional.of("test");
assertEquals("\"test\"", new String(objectMapper.writeValueAsBytes(optional), "UTF-8"));
+
+ // Kotlin module
+ IntRange range = new IntRange(1, 3);
+ assertEquals("{\"start\":1,\"end\":3}", new String(objectMapper.writeValueAsBytes(range), "UTF-8"));
}
- @Test // SPR-12634
+ @Test // SPR-12634
public void customizeWellKnownModulesWithModule() throws JsonProcessingException, UnsupportedEncodingException {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.modulesToInstall(new CustomIntegerModule()).build();
@@ -264,7 +271,7 @@ public class Jackson2ObjectMapperBuilderTests {
assertThat(new String(objectMapper.writeValueAsBytes(new Integer(4)), "UTF-8"), containsString("customid"));
}
- @Test // SPR-12634
+ @Test // SPR-12634
@SuppressWarnings("unchecked")
public void customizeWellKnownModulesWithModuleClass() throws JsonProcessingException, UnsupportedEncodingException {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modulesToInstall(CustomIntegerModule.class).build();
@@ -273,7 +280,7 @@ public class Jackson2ObjectMapperBuilderTests {
assertThat(new String(objectMapper.writeValueAsBytes(new Integer(4)), "UTF-8"), containsString("customid"));
}
- @Test // SPR-12634
+ @Test // SPR-12634
public void customizeWellKnownModulesWithSerializer() throws JsonProcessingException, UnsupportedEncodingException {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.serializerByType(Integer.class, new CustomIntegerSerializer()).build();
@@ -326,8 +333,8 @@ public class Jackson2ObjectMapperBuilderTests {
Class<?> target = String.class;
Class<?> mixInSource = Object.class;
- ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().mixIn(target,
- mixInSource).build();
+ ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules()
+ .mixIn(target, mixInSource).build();
assertEquals(1, objectMapper.mixInCount());
assertSame(mixInSource, objectMapper.findMixInClassFor(target));
@@ -340,7 +347,8 @@ public class Jackson2ObjectMapperBuilderTests {
Map<Class<?>, Class<?>> mixIns = new HashMap<Class<?>, Class<?>>();
mixIns.put(target, mixInSource);
- ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().mixIns(mixIns).build();
+ ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules()
+ .mixIns(mixIns).build();
assertEquals(1, objectMapper.mixInCount());
assertSame(mixInSource, objectMapper.findMixInClassFor(target));
@@ -437,6 +445,16 @@ public class Jackson2ObjectMapperBuilderTests {
assertTrue(xmlObjectMapper.getClass().isAssignableFrom(XmlMapper.class));
}
+ @Test // SPR-13975
+ public void defaultUseWrapper() throws JsonProcessingException {
+ ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().defaultUseWrapper(false).build();
+ assertNotNull(objectMapper);
+ assertEquals(XmlMapper.class, objectMapper.getClass());
+ ListContainer<String> container = new ListContainer<>(Arrays.asList("foo", "bar"));
+ String output = objectMapper.writeValueAsString(container);
+ assertThat(output, containsString("<list>foo</list><list>bar</list></ListContainer>"));
+ }
+
public static class CustomIntegerModule extends Module {
@@ -501,4 +519,25 @@ public class Jackson2ObjectMapperBuilderTests {
}
}
+
+ public static class ListContainer<T> {
+
+ private List<T> list;
+
+ public ListContainer() {
+ }
+
+ public ListContainer(List<T> list) {
+ this.list = list;
+ }
+
+ public List<T> getList() {
+ return list;
+ }
+
+ public void setList(List<T> list) {
+ this.list = list;
+ }
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java
index 005623f6..d678c660 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -76,6 +76,7 @@ import static org.junit.Assert.*;
* @author Sebastien Deleuze
* @author Sam Brannen
*/
+@SuppressWarnings("deprecation")
public class Jackson2ObjectMapperFactoryBeanTests {
private static final String DATE_FORMAT = "yyyy-MM-dd";
@@ -285,6 +286,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
Map<Class<?>, Class<?>> mixIns = new HashMap<Class<?>, Class<?>>();
mixIns.put(target, mixinSource);
+ this.factory.setModules(Collections.emptyList());
this.factory.setMixIns(mixIns);
this.factory.afterPropertiesSet();
ObjectMapper objectMapper = this.factory.getObject();
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java
index 687df9c9..2f334ea9 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -47,7 +47,6 @@ import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.databind.type.TypeFactory;
-
import org.junit.Before;
import org.junit.Test;
@@ -197,7 +196,7 @@ public class SpringHandlerInstantiatorTests {
return JsonTypeInfo.Id.CUSTOM;
}
- @Override
+ // Only needed when compiling against Jackson 2.7; gone in 2.8
@SuppressWarnings("deprecation")
public JavaType typeFromId(String s) {
return TypeFactory.defaultInstance().constructFromCanonical(s);
@@ -218,10 +217,15 @@ public class SpringHandlerInstantiatorTests {
return null;
}
- // New in Jackson 2.5
+ @Override
public JavaType typeFromId(DatabindContext context, String id) {
return null;
}
+
+ // New in Jackson 2.7
+ public String getDescForKnownTypeIds() {
+ return null;
+ }
}
diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java
index 4b8e60fe..b592a045 100644
--- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java
+++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -29,13 +29,12 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.util.FileCopyUtils;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
*/
public class ServletServerHttpResponseTests {
@@ -79,7 +78,6 @@ public class ServletServerHttpResponseTests {
@Test
public void preExistingHeadersFromHttpServletResponse() {
-
String headerName = "Access-Control-Allow-Origin";
String headerValue = "localhost:8080";
@@ -89,6 +87,8 @@ public class ServletServerHttpResponseTests {
assertEquals(headerValue, this.response.getHeaders().getFirst(headerName));
assertEquals(Collections.singletonList(headerValue), this.response.getHeaders().get(headerName));
assertTrue(this.response.getHeaders().containsKey(headerName));
+ assertEquals(headerValue, this.response.getHeaders().getFirst(headerName));
+ assertEquals(headerValue, this.response.getHeaders().getAccessControlAllowOrigin());
}
@Test
@@ -98,4 +98,5 @@ public class ServletServerHttpResponseTests {
assertArrayEquals("Invalid content written", content, mockResponse.getContentAsByteArray());
}
-} \ No newline at end of file
+
+}
diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java
index 48b03112..da21c6e2 100644
--- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java
+++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java
@@ -390,8 +390,8 @@ public class MockHttpServletRequest implements HttpServletRequest {
if (contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(contentType);
- if (mediaType.getCharSet() != null) {
- this.characterEncoding = mediaType.getCharSet().name();
+ if (mediaType.getCharset() != null) {
+ this.characterEncoding = mediaType.getCharset().name();
}
}
catch (Exception ex) {
diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java
index 413b3321..c0986b29 100644
--- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java
+++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java
@@ -236,8 +236,8 @@ public class MockHttpServletResponse implements HttpServletResponse {
if (contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(contentType);
- if (mediaType.getCharSet() != null) {
- this.characterEncoding = mediaType.getCharSet().name();
+ if (mediaType.getCharset() != null) {
+ this.characterEncoding = mediaType.getCharset().name();
this.charset = true;
}
}
diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java
index 224374c0..acaef598 100644
--- a/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java
+++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -25,7 +25,6 @@ import java.util.Collections;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
@@ -144,7 +143,7 @@ public class MockServletContext implements ServletContext {
private String servletContextName = "MockServletContext";
- private final Set<String> declaredRoles = new HashSet<String>();
+ private final Set<String> declaredRoles = new LinkedHashSet<String>();
private Set<SessionTrackingMode> sessionTrackingModes;
@@ -370,7 +369,6 @@ public class MockServletContext implements ServletContext {
/**
* Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher})
* that acts as a wrapper for the named Servlet.
- *
* @param name the name of the wrapped Servlet
* @param requestDispatcher the dispatcher that wraps the named Servlet
* @see #getNamedDispatcher
@@ -384,7 +382,6 @@ public class MockServletContext implements ServletContext {
/**
* Unregister the {@link RequestDispatcher} with the given name.
- *
* @param name the name of the dispatcher to unregister
* @see #getNamedDispatcher
* @see #registerNamedDispatcher
@@ -429,13 +426,13 @@ public class MockServletContext implements ServletContext {
@Override
@Deprecated
public Enumeration<Servlet> getServlets() {
- return Collections.enumeration(new HashSet<Servlet>());
+ return Collections.enumeration(Collections.<Servlet>emptySet());
}
@Override
@Deprecated
public Enumeration<String> getServletNames() {
- return Collections.enumeration(new HashSet<String>());
+ return Collections.enumeration(Collections.<String>emptySet());
}
@Override
diff --git a/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java b/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java
index 3a887601..1376734f 100644
--- a/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java
+++ b/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -127,7 +127,7 @@ public class JaxWsSupportTests {
}
catch (BeanCreationException ex) {
if ("exporter".equals(ex.getBeanName()) && ex.getRootCause() instanceof ClassNotFoundException) {
- // ignore - probably running on JDK < 1.6 without the JAX-WS impl present
+ // ignore - probably running on JDK without the JAX-WS impl present
}
else {
throw ex;
@@ -146,7 +146,7 @@ public class JaxWsSupportTests {
public OrderService myService;
- @WebServiceRef(value=OrderServiceService.class, wsdlLocation = "http://localhost:9999/OrderService?wsdl")
+ @WebServiceRef(value = OrderServiceService.class, wsdlLocation = "http://localhost:9999/OrderService?wsdl")
public void setMyService(OrderService myService) {
this.myService = myService;
}
diff --git a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java
index e9d95c2a..baabc764 100644
--- a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java
+++ b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java
@@ -30,10 +30,11 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
/**
* Test fixture for {@link ContentNegotiationManagerFactoryBean} tests.
+ *
* @author Rossen Stoyanchev
*/
public class ContentNegotiationManagerFactoryBeanTests {
@@ -119,9 +120,7 @@ public class ContentNegotiationManagerFactoryBeanTests {
assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest));
}
- // SPR-10170
-
- @Test(expected = HttpMediaTypeNotAcceptableException.class)
+ @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170
public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception {
this.factoryBean.setFavorPathExtension(true);
this.factoryBean.setIgnoreUnknownPathExtensions(false);
@@ -152,9 +151,7 @@ public class ContentNegotiationManagerFactoryBeanTests {
manager.resolveMediaTypes(this.webRequest));
}
- // SPR-10170
-
- @Test(expected = HttpMediaTypeNotAcceptableException.class)
+ @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170
public void favorParameterWithUnknownMediaType() throws HttpMediaTypeNotAcceptableException {
this.factoryBean.setFavorParameter(true);
this.factoryBean.afterPropertiesSet();
@@ -188,16 +185,12 @@ public class ContentNegotiationManagerFactoryBeanTests {
manager.resolveMediaTypes(this.webRequest));
// SPR-10513
-
this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE);
-
assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON),
manager.resolveMediaTypes(this.webRequest));
}
- // SPR-12286
-
- @Test
+ @Test // SPR-12286
public void setDefaultContentTypeWithStrategy() throws Exception {
this.factoryBean.setDefaultContentTypeStrategy(new FixedContentNegotiationStrategy(MediaType.APPLICATION_JSON));
this.factoryBean.afterPropertiesSet();
@@ -216,7 +209,6 @@ public class ContentNegotiationManagerFactoryBeanTests {
private final Map<String, String> mimeTypes = new HashMap<>();
-
public Map<String, String> getMimeTypes() {
return this.mimeTypes;
}
diff --git a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java
index 72f8aca9..f6fa0d6b 100644
--- a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java
+++ b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.web.accept;
import java.util.List;
-import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
@@ -32,21 +32,16 @@ import static org.junit.Assert.*;
* Test fixture for HeaderContentNegotiationStrategy tests.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
*/
public class HeaderContentNegotiationStrategyTests {
- private HeaderContentNegotiationStrategy strategy;
+ private final HeaderContentNegotiationStrategy strategy = new HeaderContentNegotiationStrategy();
- private NativeWebRequest webRequest;
+ private final MockHttpServletRequest servletRequest = new MockHttpServletRequest();
- private MockHttpServletRequest servletRequest;
+ private final NativeWebRequest webRequest = new ServletWebRequest(this.servletRequest);
- @Before
- public void setup() {
- this.strategy = new HeaderContentNegotiationStrategy();
- this.servletRequest = new MockHttpServletRequest();
- this.webRequest = new ServletWebRequest(servletRequest );
- }
@Test
public void resolveMediaTypes() throws Exception {
@@ -60,7 +55,20 @@ public class HeaderContentNegotiationStrategyTests {
assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString());
}
- @Test(expected=HttpMediaTypeNotAcceptableException.class)
+ @Test // SPR-14506
+ public void resolveMediaTypesFromMultipleHeaderValues() throws Exception {
+ this.servletRequest.addHeader("Accept", "text/plain; q=0.5, text/html");
+ this.servletRequest.addHeader("Accept", "text/x-dvi; q=0.8, text/x-c");
+ List<MediaType> mediaTypes = this.strategy.resolveMediaTypes(this.webRequest);
+
+ assertEquals(4, mediaTypes.size());
+ assertEquals("text/html", mediaTypes.get(0).toString());
+ assertEquals("text/x-c", mediaTypes.get(1).toString());
+ assertEquals("text/x-dvi;q=0.8", mediaTypes.get(2).toString());
+ assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString());
+ }
+
+ @Test(expected = HttpMediaTypeNotAcceptableException.class)
public void resolveMediaTypesParseError() throws Exception {
this.servletRequest.addHeader("Accept", "textplain; q=0.5");
this.strategy.resolveMediaTypes(this.webRequest);
diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java
index 32b4c0e1..508be6d3 100644
--- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java
+++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,10 +16,15 @@
package org.springframework.web.bind.support;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
import java.beans.PropertyEditorSupport;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.junit.Test;
@@ -34,8 +39,6 @@ import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.multipart.support.StringMultipartFileEditor;
-import static org.junit.Assert.*;
-
/**
* @author Juergen Hoeller
*/
@@ -126,6 +129,31 @@ public class WebRequestDataBinderTests {
assertFalse(target.isPostProcessed());
}
+ // SPR-13502
+ @Test
+ public void testCollectionFieldsDefault() throws Exception {
+ TestBean target = new TestBean();
+ target.setSomeSet(null);
+ target.setSomeList(null);
+ target.setSomeMap(null);
+ WebRequestDataBinder binder = new WebRequestDataBinder(target);
+
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.addParameter("_someSet", "visible");
+ request.addParameter("_someList", "visible");
+ request.addParameter("_someMap", "visible");
+
+ binder.bind(new ServletWebRequest(request));
+ assertThat(target.getSomeSet(), notNullValue());
+ assertThat(target.getSomeSet(), isA(Set.class));
+
+ assertThat(target.getSomeList(), notNullValue());
+ assertThat(target.getSomeList(), isA(List.class));
+
+ assertThat(target.getSomeMap(), notNullValue());
+ assertThat(target.getSomeMap(), isA(Map.class));
+ }
+
@Test
public void testFieldDefaultPreemptsFieldMarker() throws Exception {
TestBean target = new TestBean();
diff --git a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java
index 6210cec1..258a9cc4 100644
--- a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,8 +16,11 @@
package org.springframework.web.client;
+import java.io.IOException;
import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.charset.Charset;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
@@ -25,6 +28,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import org.junit.Assert;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
@@ -32,16 +36,28 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.AsyncClientHttpRequestExecution;
+import org.springframework.http.client.AsyncClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory;
+import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/**
* @author Arjen Poutsma
@@ -49,14 +65,14 @@ import static org.junit.Assert.*;
*/
public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCase {
- private final AsyncRestTemplate template = new AsyncRestTemplate(new HttpComponentsAsyncClientHttpRequestFactory());
+ private final AsyncRestTemplate template = new AsyncRestTemplate(
+ new HttpComponentsAsyncClientHttpRequestFactory());
@Test
public void getEntity() throws Exception {
- Future<ResponseEntity<String>> futureEntity =
- template.getForEntity(baseUrl + "/{method}", String.class, "get");
- ResponseEntity<String> entity = futureEntity.get();
+ Future<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get");
+ ResponseEntity<String> entity = future.get();
assertEquals("Invalid content", helloWorld, entity.getBody());
assertFalse("No headers", entity.getHeaders().isEmpty());
assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType());
@@ -65,10 +81,9 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
@Test
public void multipleFutureGets() throws Exception {
- Future<ResponseEntity<String>> futureEntity =
- template.getForEntity(baseUrl + "/{method}", String.class, "get");
- futureEntity.get();
- futureEntity.get();
+ Future<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get");
+ future.get();
+ future.get();
}
@Test
@@ -88,9 +103,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- // wait till done
- while (!futureEntity.isDone()) {
- }
+ waitTillDone(futureEntity);
}
@Test
@@ -103,9 +116,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType());
assertEquals("Invalid status code", HttpStatus.OK, entity.getStatusCode());
}, ex -> fail(ex.getMessage()));
- // wait till done
- while (!futureEntity.isDone()) {
- }
+ waitTillDone(futureEntity);
}
@Test
@@ -160,8 +171,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!headersFuture.isDone()) {
- }
+ waitTillDone(headersFuture);
}
@Test
@@ -169,15 +179,14 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
ListenableFuture<HttpHeaders> headersFuture = template.headForHeaders(baseUrl + "/get");
headersFuture.addCallback(result -> assertTrue("No Content-Type header",
result.containsKey("Content-Type")), ex -> fail(ex.getMessage()));
- while (!headersFuture.isDone()) {
- }
+ waitTillDone(headersFuture);
}
@Test
public void postForLocation() throws Exception {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15")));
- HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders);
+ HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
Future<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post");
URI location = locationFuture.get();
assertEquals("Invalid location", new URI(baseUrl + "/post/1"), location);
@@ -187,7 +196,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
public void postForLocationCallback() throws Exception {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15")));
- HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders);
+ HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
final URI expected = new URI(baseUrl + "/post/1");
ListenableFuture<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post");
locationFuture.addCallback(new ListenableFutureCallback<URI>() {
@@ -200,21 +209,19 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!locationFuture.isDone()) {
- }
+ waitTillDone(locationFuture);
}
@Test
public void postForLocationCallbackWithLambdas() throws Exception {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15")));
- HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders);
+ HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
final URI expected = new URI(baseUrl + "/post/1");
ListenableFuture<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post");
locationFuture.addCallback(result -> assertEquals("Invalid location", expected, result),
ex -> fail(ex.getMessage()));
- while (!locationFuture.isDone()) {
- }
+ waitTillDone(locationFuture);
}
@Test
@@ -241,8 +248,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!responseEntityFuture.isDone()) {
- }
+ waitTillDone(responseEntityFuture);
}
@Test
@@ -250,10 +256,10 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld);
ListenableFuture<ResponseEntity<String>> responseEntityFuture =
template.postForEntity(baseUrl + "/{method}", requestEntity, String.class, "post");
- responseEntityFuture.addCallback(result -> assertEquals("Invalid content", helloWorld, result.getBody()),
+ responseEntityFuture.addCallback(
+ result -> assertEquals("Invalid content", helloWorld, result.getBody()),
ex -> fail(ex.getMessage()));
- while (!responseEntityFuture.isDone()) {
- }
+ waitTillDone(responseEntityFuture);
}
@Test
@@ -277,8 +283,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!responseEntityFuture.isDone()) {
- }
+ waitTillDone(responseEntityFuture);
}
@Test
@@ -300,16 +305,14 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!deletedFuture.isDone()) {
- }
+ waitTillDone(deletedFuture);
}
@Test
public void deleteCallbackWithLambdas() throws Exception {
ListenableFuture<?> deletedFuture = template.delete(new URI(baseUrl + "/delete"));
- deletedFuture.addCallback(result -> assertNull(result), ex -> fail(ex.getMessage()));
- while (!deletedFuture.isDone()) {
- }
+ deletedFuture.addCallback(Assert::assertNull, ex -> fail(ex.getMessage()));
+ waitTillDone(deletedFuture);
}
@Test
@@ -377,8 +380,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
assertNotNull(ex.getResponseBodyAsString());
}
});
- while (!future.isDone()) {
- }
+ waitTillDone(future);
}
@Test
@@ -391,8 +393,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
assertNotNull(hcex.getStatusText());
assertNotNull(hcex.getResponseBodyAsString());
});
- while (!future.isDone()) {
- }
+ waitTillDone(future);
}
@Test
@@ -429,8 +430,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
assertNotNull(hsex.getResponseBodyAsString());
}
});
- while (!future.isDone()) {
- }
+ waitTillDone(future);
}
@Test
@@ -443,8 +443,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
assertNotNull(hsex.getStatusText());
assertNotNull(hsex.getResponseBodyAsString());
});
- while (!future.isDone()) {
- }
+ waitTillDone(future);
}
@Test
@@ -469,8 +468,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!allowedFuture.isDone()) {
- }
+ waitTillDone(allowedFuture);
}
@Test
@@ -479,8 +477,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
allowedFuture.addCallback(result -> assertEquals("Invalid response",
EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD,HttpMethod.TRACE), result),
ex -> fail(ex.getMessage()));
- while (!allowedFuture.isDone()) {
- }
+ waitTillDone(allowedFuture);
}
@Test
@@ -513,8 +510,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!responseFuture.isDone()) {
- }
+ waitTillDone(responseFuture);
}
@Test
@@ -527,8 +523,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, String.class, "get");
responseFuture.addCallback(result -> assertEquals("Invalid content", helloWorld,
result.getBody()), ex -> fail(ex.getMessage()));
- while (!responseFuture.isDone()) {
- }
+ waitTillDone(responseFuture);
}
@Test
@@ -536,7 +531,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("MyHeader", "MyValue");
requestHeaders.setContentType(MediaType.TEXT_PLAIN);
- HttpEntity<String> requestEntity = new HttpEntity<String>(helloWorld, requestHeaders);
+ HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders);
Future<ResponseEntity<Void>> resultFuture =
template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post");
ResponseEntity<Void> result = resultFuture.get();
@@ -550,7 +545,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("MyHeader", "MyValue");
requestHeaders.setContentType(MediaType.TEXT_PLAIN);
- HttpEntity<String> requestEntity = new HttpEntity<String>(helloWorld, requestHeaders);
+ HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders);
ListenableFuture<ResponseEntity<Void>> resultFuture =
template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post");
final URI expected =new URI(baseUrl + "/post/1");
@@ -565,8 +560,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
fail(ex.getMessage());
}
});
- while (!resultFuture.isDone()) {
- }
+ waitTillDone(resultFuture);
}
@Test
@@ -574,7 +568,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("MyHeader", "MyValue");
requestHeaders.setContentType(MediaType.TEXT_PLAIN);
- HttpEntity<String> requestEntity = new HttpEntity<String>(helloWorld, requestHeaders);
+ HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders);
ListenableFuture<ResponseEntity<Void>> resultFuture =
template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post");
final URI expected =new URI(baseUrl + "/post/1");
@@ -582,13 +576,12 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
assertEquals("Invalid location", expected, result.getHeaders().getLocation());
assertFalse(result.hasBody());
}, ex -> fail(ex.getMessage()));
- while (!resultFuture.isDone()) {
- }
+ waitTillDone(resultFuture);
}
@Test
public void multipart() throws Exception {
- MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
+ MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("name 1", "value 1");
parts.add("name 2", "value 2+1");
parts.add("name 2", "value 2+2");
@@ -600,4 +593,73 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
future.get();
}
+ @Test
+ public void getAndInterceptResponse() throws Exception {
+ RequestInterceptor interceptor = new RequestInterceptor();
+ template.setInterceptors(Collections.singletonList(interceptor));
+ ListenableFuture<ResponseEntity<String>> future = template.getForEntity("/get", String.class);
+
+ interceptor.latch.await(5, TimeUnit.SECONDS);
+ assertNotNull(interceptor.response);
+ assertEquals(HttpStatus.OK, interceptor.response.getStatusCode());
+ assertNull(interceptor.exception);
+ assertEquals(helloWorld, future.get().getBody());
+ }
+
+ @Test
+ public void getAndInterceptError() throws Exception {
+ RequestInterceptor interceptor = new RequestInterceptor();
+ template.setInterceptors(Collections.singletonList(interceptor));
+ template.getForEntity("/status/notfound", String.class);
+
+ interceptor.latch.await(5, TimeUnit.SECONDS);
+ assertNotNull(interceptor.response);
+ assertEquals(HttpStatus.NOT_FOUND, interceptor.response.getStatusCode());
+ assertNull(interceptor.exception);
+ }
+
+ private void waitTillDone(ListenableFuture<?> future) {
+ while (!future.isDone()) {
+ }
+ }
+
+
+ private static class RequestInterceptor implements AsyncClientHttpRequestInterceptor {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ private volatile ClientHttpResponse response;
+
+ private volatile Throwable exception;
+
+ @Override
+ public ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body,
+ AsyncClientHttpRequestExecution execution) throws IOException {
+
+ request = new HttpRequestWrapper(request) {
+
+ @Override
+ public URI getURI() {
+ try {
+ return new URI(baseUrl + super.getURI().toString());
+ }
+ catch (URISyntaxException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+ };
+
+ ListenableFuture<ClientHttpResponse> future = execution.executeAsync(request, body);
+ future.addCallback(
+ resp -> {
+ response = resp;
+ this.latch.countDown();
+ },
+ ex -> {
+ exception = ex;
+ this.latch.countDown();
+ });
+ return future;
+ }
+ }
}
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
index 149340b6..d5f00fc2 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -20,11 +20,16 @@ import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
+import java.util.ArrayList;
import java.util.EnumSet;
+import java.util.List;
import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.JsonTypeName;
import org.junit.Test;
+import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
@@ -32,6 +37,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
+import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJacksonValue;
@@ -215,7 +221,7 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase {
bean.setWith2("with");
bean.setWithout("without");
HttpEntity<MySampleBean> entity = new HttpEntity<MySampleBean>(bean, entityHeaders);
- String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post");
+ String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class);
assertTrue(s.contains("\"with1\":\"with\""));
assertTrue(s.contains("\"with2\":\"with\""));
assertTrue(s.contains("\"without\":\"without\""));
@@ -229,7 +235,7 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase {
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
jacksonValue.setSerializationView(MyJacksonView1.class);
HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(jacksonValue, entityHeaders);
- String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post");
+ String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class);
assertTrue(s.contains("\"with1\":\"with\""));
assertFalse(s.contains("\"with2\":\"with\""));
assertFalse(s.contains("\"without\":\"without\""));
@@ -243,6 +249,21 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase {
assertEquals("Invalid content", helloWorld, s);
}
+ @Test // SPR-13154
+ public void jsonPostForObjectWithJacksonTypeInfoList() throws URISyntaxException {
+ List<ParentClass> list = new ArrayList<>();
+ list.add(new Foo("foo"));
+ list.add(new Bar("bar"));
+ ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<ParentClass>>() {};
+ RequestEntity<List<ParentClass>> entity = RequestEntity
+ .post(new URI(baseUrl + "/jsonpost"))
+ .contentType(new MediaType("application", "json", Charset.forName("UTF-8")))
+ .body(list, typeReference.getType());
+ String content = template.exchange(entity, String.class).getBody();
+ assertTrue(content.contains("\"type\":\"foo\""));
+ assertTrue(content.contains("\"type\":\"bar\""));
+ }
+
public interface MyJacksonView1 {};
public interface MyJacksonView2 {};
@@ -290,4 +311,47 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase {
}
}
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
+ public static class ParentClass {
+
+ private String parentProperty;
+
+ public ParentClass() {
+ }
+
+ public ParentClass(String parentProperty) {
+ this.parentProperty = parentProperty;
+ }
+
+ public String getParentProperty() {
+ return parentProperty;
+ }
+
+ public void setParentProperty(String parentProperty) {
+ this.parentProperty = parentProperty;
+ }
+ }
+
+ @JsonTypeName("foo")
+ public static class Foo extends ParentClass {
+
+ public Foo() {
+ }
+
+ public Foo(String parentProperty) {
+ super(parentProperty);
+ }
+ }
+
+ @JsonTypeName("bar")
+ public static class Bar extends ParentClass {
+
+ public Bar() {
+ }
+
+ public Bar(String parentProperty) {
+ super(parentProperty);
+ }
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
index 1ecfc48e..5674ad55 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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
+ * 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,
@@ -49,6 +49,7 @@ import static org.mockito.BDDMockito.*;
/**
* @author Arjen Poutsma
+ * @author Rossen Stoyanchev
*/
@SuppressWarnings("unchecked")
public class RestTemplateTests {
@@ -135,7 +136,7 @@ public class RestTemplateTests {
given(response.getStatusCode()).willReturn(status);
given(response.getStatusText()).willReturn(status.getReasonPhrase());
- Map<String, String> vars = new HashMap<String, String>(2);
+ Map<String, String> vars = new HashMap<>(2);
vars.put("first", null);
vars.put("last", "foo");
template.execute("http://example.com/{first}-{last}", HttpMethod.GET, null, null, vars);
@@ -278,7 +279,7 @@ public class RestTemplateTests {
given(response.getHeaders()).willReturn(new HttpHeaders());
given(response.getBody()).willReturn(null);
- Map<String, String> uriVariables = new HashMap<String, String>(2);
+ Map<String, String> uriVariables = new HashMap<>(2);
uriVariables.put("hotel", "1");
uriVariables.put("publicpath", "pics/logo.png");
uriVariables.put("scale", "150x150");
@@ -351,7 +352,7 @@ public class RestTemplateTests {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.setContentType(contentType);
- HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders);
+ HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
URI result = template.postForLocation("http://example.com", entity);
assertEquals("Invalid POST result", expected, result);
@@ -379,7 +380,7 @@ public class RestTemplateTests {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.set("MyHeader", "MyValue");
- HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders);
+ HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);
URI result = template.postForLocation("http://example.com", entity);
assertEquals("Invalid POST result", expected, result);
@@ -622,21 +623,27 @@ public class RestTemplateTests {
verify(response).close();
}
+ // Issue: SPR-9325, SPR-13860
+
@Test
public void ioException() throws Exception {
+ String url = "http://example.com/resource?access_token=123";
+
given(converter.canRead(String.class, null)).willReturn(true);
MediaType mediaType = new MediaType("foo", "bar");
given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(mediaType));
- given(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).willReturn(request);
+ given(requestFactory.createRequest(new URI(url), HttpMethod.GET)).willReturn(request);
given(request.getHeaders()).willReturn(new HttpHeaders());
- given(request.execute()).willThrow(new IOException());
+ given(request.execute()).willThrow(new IOException("Socket failure"));
try {
- template.getForObject("http://example.com/resource", String.class);
+ template.getForObject(url, String.class);
fail("RestClientException expected");
}
catch (ResourceAccessException ex) {
- // expected
+ assertEquals("I/O error on GET request for \"http://example.com/resource\": " +
+ "Socket failure; nested exception is java.io.IOException: Socket failure",
+ ex.getMessage());
}
}
@@ -669,7 +676,7 @@ public class RestTemplateTests {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.set("MyHeader", "MyValue");
- HttpEntity<String> requestEntity = new HttpEntity<String>(body, entityHeaders);
+ HttpEntity<String> requestEntity = new HttpEntity<>(body, entityHeaders);
ResponseEntity<Integer> result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, Integer.class);
assertEquals("Invalid POST result", expected, result.getBody());
assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
@@ -692,9 +699,9 @@ public class RestTemplateTests {
given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).willReturn(this.request);
HttpHeaders requestHeaders = new HttpHeaders();
given(this.request.getHeaders()).willReturn(requestHeaders);
- given(converter.canWrite(String.class, null)).willReturn(true);
+ given(converter.canWrite(String.class, String.class, null)).willReturn(true);
String requestBody = "Hello World";
- converter.write(requestBody, null, this.request);
+ converter.write(requestBody, String.class, null, this.request);
given(this.request.execute()).willReturn(response);
given(errorHandler.hasError(response)).willReturn(false);
List<Integer> expected = Collections.singletonList(42);
@@ -703,7 +710,7 @@ public class RestTemplateTests {
responseHeaders.setContentLength(10);
given(response.getStatusCode()).willReturn(HttpStatus.OK);
given(response.getHeaders()).willReturn(responseHeaders);
- given(response.getBody()).willReturn(new ByteArrayInputStream(new Integer(42).toString().getBytes()));
+ given(response.getBody()).willReturn(new ByteArrayInputStream(Integer.toString(42).getBytes()));
given(converter.canRead(intList.getType(), null, MediaType.TEXT_PLAIN)).willReturn(true);
given(converter.read(eq(intList.getType()), eq(null), any(HttpInputMessage.class))).willReturn(expected);
given(response.getStatusCode()).willReturn(HttpStatus.OK);
@@ -713,7 +720,7 @@ public class RestTemplateTests {
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.set("MyHeader", "MyValue");
- HttpEntity<String> requestEntity = new HttpEntity<String>(requestBody, entityHeaders);
+ HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, entityHeaders);
ResponseEntity<List<Integer>> result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, intList);
assertEquals("Invalid POST result", expected, result.getBody());
assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java
index ef1afaad..603e2642 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -78,8 +78,7 @@ public class ServletRequestAttributesTests {
request.setSession(session);
ServletRequestAttributes attrs = new ServletRequestAttributes(request);
attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_SESSION);
- Object value = session.getAttribute(KEY);
- assertSame(VALUE, value);
+ assertSame(VALUE, session.getAttribute(KEY));
}
@Test
@@ -89,11 +88,11 @@ public class ServletRequestAttributesTests {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setSession(session);
ServletRequestAttributes attrs = new ServletRequestAttributes(request);
+ assertSame(VALUE, attrs.getAttribute(KEY, RequestAttributes.SCOPE_SESSION));
attrs.requestCompleted();
request.close();
attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_SESSION);
- Object value = session.getAttribute(KEY);
- assertSame(VALUE, value);
+ assertSame(VALUE, session.getAttribute(KEY));
}
@Test
@@ -104,8 +103,7 @@ public class ServletRequestAttributesTests {
request.setSession(session);
ServletRequestAttributes attrs = new ServletRequestAttributes(request);
attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_GLOBAL_SESSION);
- Object value = session.getAttribute(KEY);
- assertSame(VALUE, value);
+ assertSame(VALUE, session.getAttribute(KEY));
}
@Test
@@ -115,11 +113,11 @@ public class ServletRequestAttributesTests {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setSession(session);
ServletRequestAttributes attrs = new ServletRequestAttributes(request);
+ assertSame(VALUE, attrs.getAttribute(KEY, RequestAttributes.SCOPE_GLOBAL_SESSION));
attrs.requestCompleted();
request.close();
attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_GLOBAL_SESSION);
- Object value = session.getAttribute(KEY);
- assertSame(VALUE, value);
+ assertSame(VALUE, session.getAttribute(KEY));
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
index 3832ae3a..a5a8b07c 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,6 +16,8 @@
package org.springframework.web.context.request;
+import static org.junit.Assert.*;
+
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
@@ -32,8 +34,6 @@ import org.junit.runners.Parameterized.Parameters;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
-import static org.junit.Assert.*;
-
/**
* Parameterized tests for ServletWebRequest
* @author Juergen Hoeller
@@ -145,6 +145,18 @@ public class ServletWebRequestHttpMethodsTests {
}
@Test
+ public void checkNotModifiedETagWithSeparatorChars() {
+ String eTag = "\"Foo, Bar\"";
+ servletRequest.addHeader("If-None-Match", eTag);
+
+ assertTrue(request.checkNotModified(eTag));
+
+ assertEquals(304, servletResponse.getStatus());
+ assertEquals(eTag, servletResponse.getHeader("ETag"));
+ }
+
+
+ @Test
public void checkModifiedETag() {
String currentETag = "\"Foo\"";
String oldEtag = "Bar";
@@ -204,6 +216,7 @@ public class ServletWebRequestHttpMethodsTests {
assertEquals(dateFormat.format(currentDate.getTime()), servletResponse.getHeader("Last-Modified"));
}
+ // SPR-14224
@Test
public void checkNotModifiedETagAndModifiedTimestamp() {
String eTag = "\"Foo\"";
@@ -212,9 +225,9 @@ public class ServletWebRequestHttpMethodsTests {
long oneMinuteAgo = currentEpoch - (1000 * 60);
servletRequest.addHeader("If-Modified-Since", oneMinuteAgo);
- assertFalse(request.checkNotModified(eTag, currentEpoch));
+ assertTrue(request.checkNotModified(eTag, currentEpoch));
- assertEquals(200, servletResponse.getStatus());
+ assertEquals(304, servletResponse.getStatus());
assertEquals(eTag, servletResponse.getHeader("ETag"));
assertEquals(dateFormat.format(currentEpoch), servletResponse.getHeader("Last-Modified"));
}
@@ -293,4 +306,28 @@ public class ServletWebRequestHttpMethodsTests {
assertEquals(dateFormat.format(epochTime), servletResponse.getHeader("Last-Modified"));
}
+ @Test
+ public void checkNotModifiedTimestampConditionalPut() throws Exception {
+ long currentEpoch = currentDate.getTime();
+ long oneMinuteAgo = currentEpoch - (1000 * 60);
+ servletRequest.setMethod("PUT");
+ servletRequest.addHeader("If-UnModified-Since", currentEpoch);
+
+ assertFalse(request.checkNotModified(oneMinuteAgo));
+ assertEquals(200, servletResponse.getStatus());
+ assertEquals(null, servletResponse.getHeader("Last-Modified"));
+ }
+
+ @Test
+ public void checkNotModifiedTimestampConditionalPutConflict() throws Exception {
+ long currentEpoch = currentDate.getTime();
+ long oneMinuteAgo = currentEpoch - (1000 * 60);
+ servletRequest.setMethod("PUT");
+ servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo);
+
+ assertTrue(request.checkNotModified(currentEpoch));
+ assertEquals(412, servletResponse.getStatus());
+ assertEquals(null, servletResponse.getHeader("Last-Modified"));
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java b/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java
index ab198238..a44318fc 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java
@@ -172,6 +172,11 @@ public class SessionScopeTests {
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
}
+
+ @Override
+ public boolean requiresDestruction(Object bean) {
+ return true;
+ }
}
@@ -195,6 +200,11 @@ public class SessionScopeTests {
((BeanNameAware) bean).setBeanName(null);
}
}
+
+ @Override
+ public boolean requiresDestruction(Object bean) {
+ return true;
+ }
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
index 50d84230..2596796e 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
@@ -147,7 +147,7 @@ public class StandardServletAsyncWebRequestTests {
}
// SPR-13292
-
+
@Test
public void onCompletionHandlerAfterOnErrorEvent() throws Exception {
Runnable handler = mock(Runnable.class);
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java
index c763eeec..d4a2ad47 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java
@@ -150,8 +150,9 @@ public class WebAsyncManagerTests {
try {
this.asyncManager.startCallableProcessing(task);
fail("Expected Exception");
- }catch(Exception e) {
- assertEquals(exception, e);
+ }
+ catch (Exception ex) {
+ assertEquals(exception, ex);
}
assertFalse(this.asyncManager.hasConcurrentResult());
@@ -162,7 +163,6 @@ public class WebAsyncManagerTests {
@Test
public void startCallableProcessingPreProcessException() throws Exception {
-
Callable<Object> task = new StubCallable(21);
Exception exception = new Exception();
@@ -183,7 +183,6 @@ public class WebAsyncManagerTests {
@Test
public void startCallableProcessingPostProcessException() throws Exception {
-
Callable<Object> task = new StubCallable(21);
Exception exception = new Exception();
@@ -205,7 +204,6 @@ public class WebAsyncManagerTests {
@Test
public void startCallableProcessingPostProcessContinueAfterException() throws Exception {
-
Callable<Object> task = new StubCallable(21);
Exception exception = new Exception();
@@ -231,7 +229,6 @@ public class WebAsyncManagerTests {
@Test
public void startCallableProcessingWithAsyncTask() throws Exception {
-
AsyncTaskExecutor executor = mock(AsyncTaskExecutor.class);
given(this.asyncWebRequest.getNativeRequest(HttpServletRequest.class)).willReturn(this.servletRequest);
@@ -259,7 +256,6 @@ public class WebAsyncManagerTests {
@Test
public void startDeferredResultProcessing() throws Exception {
-
DeferredResult<String> deferredResult = new DeferredResult<String>(1000L);
String concurrentResult = "abc";
@@ -282,7 +278,6 @@ public class WebAsyncManagerTests {
@Test
public void startDeferredResultProcessingBeforeConcurrentHandlingException() throws Exception {
-
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>();
Exception exception = new Exception();
@@ -295,7 +290,7 @@ public class WebAsyncManagerTests {
this.asyncManager.startDeferredResultProcessing(deferredResult);
fail("Expected Exception");
}
- catch(Exception success) {
+ catch (Exception success) {
assertEquals(exception, success);
}
@@ -328,7 +323,6 @@ public class WebAsyncManagerTests {
@Test
public void startDeferredResultProcessingPostProcessException() throws Exception {
-
DeferredResult<Integer> deferredResult = new DeferredResult<Integer>();
Exception exception = new Exception();
@@ -371,6 +365,7 @@ public class WebAsyncManagerTests {
verify(this.asyncWebRequest).dispatch();
}
+
private final class StubCallable implements Callable<Object> {
private Object value;
@@ -388,6 +383,7 @@ public class WebAsyncManagerTests {
}
}
+
@SuppressWarnings("serial")
private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor {
diff --git a/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java b/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java
index a712e525..dd56ff00 100644
--- a/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java
@@ -49,7 +49,8 @@ public class Spr8510Tests {
try {
cll.contextInitialized(new ServletContextEvent(sc));
fail("expected exception");
- } catch (Throwable t) {
+ }
+ catch (Throwable t) {
// assert that an attempt was made to load the correct XML
assertTrue(t.getMessage(), t.getMessage().endsWith(
"Could not open ServletContext resource [/programmatic.xml]"));
@@ -75,7 +76,8 @@ public class Spr8510Tests {
try {
cll.contextInitialized(new ServletContextEvent(sc));
fail("expected exception");
- } catch (Throwable t) {
+ }
+ catch (Throwable t) {
// assert that an attempt was made to load the correct XML
assertTrue(t.getMessage(), t.getMessage().endsWith(
"Could not open ServletContext resource [/from-init-param.xml]"));
@@ -98,7 +100,8 @@ public class Spr8510Tests {
try {
cll.contextInitialized(new ServletContextEvent(sc));
fail("expected exception");
- } catch (Throwable t) {
+ }
+ catch (Throwable t) {
// assert that an attempt was made to load the correct XML
assertTrue(t.getMessage().endsWith(
"Could not open ServletContext resource [/from-init-param.xml]"));
@@ -125,7 +128,8 @@ public class Spr8510Tests {
try {
cll.contextInitialized(new ServletContextEvent(sc));
fail("expected exception");
- } catch (Throwable t) {
+ }
+ catch (Throwable t) {
// assert that an attempt was made to load the correct XML
System.out.println(t.getMessage());
assertTrue(t.getMessage().endsWith(
@@ -150,7 +154,8 @@ public class Spr8510Tests {
try {
cll.contextInitialized(new ServletContextEvent(sc));
fail("expected exception");
- } catch (Throwable t) {
+ }
+ catch (Throwable t) {
// assert that an attempt was made to load the correct XML
System.out.println(t.getMessage());
assertTrue(t.getMessage().endsWith(
diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
index 8d3651c5..32f68af3 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -176,7 +176,7 @@ public class CorsConfigurationTests {
@Test
public void checkMethodAllowed() {
- assertEquals(Arrays.asList(HttpMethod.GET), config.checkHttpMethod(HttpMethod.GET));
+ assertEquals(Arrays.asList(HttpMethod.GET, HttpMethod.HEAD), config.checkHttpMethod(HttpMethod.GET));
config.addAllowedMethod("GET");
assertEquals(Arrays.asList(HttpMethod.GET), config.checkHttpMethod(HttpMethod.GET));
config.addAllowedMethod("POST");
@@ -189,7 +189,7 @@ public class CorsConfigurationTests {
assertNull(config.checkHttpMethod(null));
assertNull(config.checkHttpMethod(HttpMethod.DELETE));
config.setAllowedMethods(new ArrayList<>());
- assertNull(config.checkHttpMethod(HttpMethod.HEAD));
+ assertNull(config.checkHttpMethod(HttpMethod.POST));
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
index 56ab6166..30a93e30 100644
--- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -171,7 +171,7 @@ public class DefaultCorsProcessorTests {
this.conf.addAllowedOrigin("*");
this.processor.processRequest(this.conf, request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
- assertEquals("GET", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS));
+ assertEquals("GET,HEAD", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS));
}
@Test
diff --git a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java
index 64392f91..5f0006c9 100644
--- a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java
+++ b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -145,4 +145,26 @@ public class CharacterEncodingFilterTests {
verify(filterChain).doFilter(request, response);
}
+ // SPR-14240
+ @Test
+ public void setForceEncodingOnRequestOnly() throws Exception {
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ request.setCharacterEncoding(ENCODING);
+ given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null);
+ given(request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).willReturn(null);
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ FilterChain filterChain = mock(FilterChain.class);
+
+ CharacterEncodingFilter filter = new CharacterEncodingFilter(ENCODING, true, false);
+ filter.init(new MockFilterConfig(FILTER_NAME));
+ filter.doFilter(request, response, filterChain);
+
+ verify(request).setAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE);
+ verify(request).removeAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX);
+ verify(request, times(2)).setCharacterEncoding(ENCODING);
+ verify(response, never()).setCharacterEncoding(ENCODING);
+ verify(filterChain).doFilter(request, response);
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java
new file mode 100644
index 00000000..56d93f8c
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2002-2016 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.web.filter;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.mock.web.test.MockFilterChain;
+import org.springframework.mock.web.test.MockHttpServletRequest;
+import org.springframework.mock.web.test.MockHttpServletResponse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for {@link ForwardedHeaderFilter}.
+ * @author Rossen Stoyanchev
+ * @author Eddú Meléndez
+ */
+public class ForwardedHeaderFilterTests {
+
+ private static final String X_FORWARDED_PROTO = "x-forwarded-proto"; // SPR-14372 (case insensitive)
+ private static final String X_FORWARDED_HOST = "x-forwarded-host";
+ private static final String X_FORWARDED_PORT = "x-forwarded-port";
+ private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix";
+
+
+ private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
+
+ private MockHttpServletRequest request;
+
+ private MockFilterChain filterChain;
+
+
+ @Before
+ @SuppressWarnings("serial")
+ public void setUp() throws Exception {
+ this.request = new MockHttpServletRequest();
+ this.request.setScheme("http");
+ this.request.setServerName("localhost");
+ this.request.setServerPort(80);
+ this.filterChain = new MockFilterChain(new HttpServlet() {});
+ }
+
+
+ @Test
+ public void contextPathEmpty() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "");
+ assertEquals("", filterAndGetContextPath());
+ }
+
+ @Test
+ public void contextPathWithTrailingSlash() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/foo/bar/");
+ assertEquals("/foo/bar", filterAndGetContextPath());
+ }
+
+ @Test
+ public void contextPathWithTrailingSlashes() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/foo/bar/baz///");
+ assertEquals("/foo/bar/baz", filterAndGetContextPath());
+ }
+
+ @Test
+ public void requestUri() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/");
+ this.request.setContextPath("/app");
+ this.request.setRequestURI("/app/path");
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+
+ assertEquals("", actual.getContextPath());
+ assertEquals("/path", actual.getRequestURI());
+ }
+
+ @Test
+ public void requestUriWithTrailingSlash() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/");
+ this.request.setContextPath("/app");
+ this.request.setRequestURI("/app/path/");
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+
+ assertEquals("", actual.getContextPath());
+ assertEquals("/path/", actual.getRequestURI());
+ }
+ @Test
+ public void requestUriEqualsContextPath() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/");
+ this.request.setContextPath("/app");
+ this.request.setRequestURI("/app");
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+
+ assertEquals("", actual.getContextPath());
+ assertEquals("/", actual.getRequestURI());
+ }
+
+ @Test
+ public void requestUriRootUrl() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/");
+ this.request.setContextPath("/app");
+ this.request.setRequestURI("/app/");
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+
+ assertEquals("", actual.getContextPath());
+ assertEquals("/", actual.getRequestURI());
+ }
+
+ @Test
+ public void caseInsensitiveForwardedPrefix() throws Exception {
+ this.request = new MockHttpServletRequest() {
+
+ // Make it case-sensitive (SPR-14372)
+
+ @Override
+ public String getHeader(String header) {
+ Enumeration<String> names = getHeaderNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ if (name.equals(header)) {
+ return super.getHeader(header);
+ }
+ }
+ return null;
+ }
+ };
+ this.request.addHeader(X_FORWARDED_PREFIX, "/prefix");
+ this.request.setRequestURI("/path");
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+
+ assertEquals("/prefix/path", actual.getRequestURI());
+ }
+
+ @Test
+ public void shouldFilter() throws Exception {
+ testShouldFilter("Forwarded");
+ testShouldFilter(X_FORWARDED_HOST);
+ testShouldFilter(X_FORWARDED_PORT);
+ testShouldFilter(X_FORWARDED_PROTO);
+ }
+
+ @Test
+ public void shouldNotFilter() throws Exception {
+ assertTrue(this.filter.shouldNotFilter(new MockHttpServletRequest()));
+ }
+
+ @Test
+ public void forwardedRequest() throws Exception {
+ this.request.setRequestURI("/mvc-showcase");
+ this.request.addHeader(X_FORWARDED_PROTO, "https");
+ this.request.addHeader(X_FORWARDED_HOST, "84.198.58.199");
+ this.request.addHeader(X_FORWARDED_PORT, "443");
+ this.request.addHeader("foo", "bar");
+
+ this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
+ HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest();
+
+ assertEquals("https://84.198.58.199/mvc-showcase", actual.getRequestURL().toString());
+ assertEquals("https", actual.getScheme());
+ assertEquals("84.198.58.199", actual.getServerName());
+ assertEquals(443, actual.getServerPort());
+ assertTrue(actual.isSecure());
+
+ assertNull(actual.getHeader(X_FORWARDED_PROTO));
+ assertNull(actual.getHeader(X_FORWARDED_HOST));
+ assertNull(actual.getHeader(X_FORWARDED_PORT));
+ assertEquals("bar", actual.getHeader("foo"));
+ }
+
+ @Test
+ public void requestUriWithForwardedPrefix() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/prefix");
+ this.request.setRequestURI("/mvc-showcase");
+
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+ assertEquals("http://localhost/prefix/mvc-showcase", actual.getRequestURL().toString());
+ }
+
+ @Test
+ public void requestUriWithForwardedPrefixTrailingSlash() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/prefix/");
+ this.request.setRequestURI("/mvc-showcase");
+
+ HttpServletRequest actual = filterAndGetWrappedRequest();
+ assertEquals("http://localhost/prefix/mvc-showcase", actual.getRequestURL().toString());
+ }
+
+ @Test
+ public void contextPathWithForwardedPrefix() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/prefix");
+ this.request.setContextPath("/mvc-showcase");
+
+ String actual = filterAndGetContextPath();
+ assertEquals("/prefix", actual);
+ }
+
+ @Test
+ public void contextPathWithForwardedPrefixTrailingSlash() throws Exception {
+ this.request.addHeader(X_FORWARDED_PREFIX, "/prefix/");
+ this.request.setContextPath("/mvc-showcase");
+
+ String actual = filterAndGetContextPath();
+ assertEquals("/prefix", actual);
+ }
+
+ private String filterAndGetContextPath() throws ServletException, IOException {
+ return filterAndGetWrappedRequest().getContextPath();
+ }
+
+ private HttpServletRequest filterAndGetWrappedRequest() throws ServletException, IOException {
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ this.filter.doFilterInternal(this.request, response, this.filterChain);
+ return (HttpServletRequest) this.filterChain.getRequest();
+ }
+
+ private void testShouldFilter(String headerName) throws ServletException {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.addHeader(headerName, "1");
+ assertFalse(this.filter.shouldNotFilter(request));
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java
index 0c5af485..893501b2 100644
--- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java
+++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -74,6 +74,26 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
+ public void filterNoMatchWeakETag() throws Exception {
+ this.filter.setWriteWeakETag(true);
+ final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ final byte[] responseBody = "Hello World".getBytes("UTF-8");
+ FilterChain filterChain = (filterRequest, filterResponse) -> {
+ assertEquals("Invalid request passed", request, filterRequest);
+ ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
+ FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
+ };
+ filter.doFilter(request, response, filterChain);
+
+ assertEquals("Invalid status", 200, response.getStatus());
+ assertEquals("Invalid ETag header", "W/\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
+ assertTrue("Invalid Content-Length header", response.getContentLength() > 0);
+ assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
+ }
+
+ @Test
public void filterMatch() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
@@ -95,6 +115,27 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
+ public void filterMatchWeakEtag() throws Exception {
+ final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
+ String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
+ request.addHeader("If-None-Match", "W/" + etag);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ FilterChain filterChain = (filterRequest, filterResponse) -> {
+ assertEquals("Invalid request passed", request, filterRequest);
+ byte[] responseBody = "Hello World".getBytes("UTF-8");
+ FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
+ filterResponse.setContentLength(responseBody.length);
+ };
+ filter.doFilter(request, response, filterChain);
+
+ assertEquals("Invalid status", 304, response.getStatus());
+ assertEquals("Invalid ETag header", "\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
+ assertFalse("Response has Content-Length header", response.containsHeader("Content-Length"));
+ assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
+ }
+
+ @Test
public void filterWriter() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
index 21fc00e8..2010ba9d 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
@@ -24,6 +24,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.tests.sample.beans.TestBean;
import org.springframework.validation.BindException;
@@ -51,56 +52,56 @@ import static org.mockito.BDDMockito.*;
*/
public class ModelAttributeMethodProcessorTests {
+ private NativeWebRequest request;
+
+ private ModelAndViewContainer container;
+
private ModelAttributeMethodProcessor processor;
private MethodParameter paramNamedValidModelAttr;
-
private MethodParameter paramErrors;
-
private MethodParameter paramInt;
-
private MethodParameter paramModelAttr;
-
+ private MethodParameter paramBindingDisabledAttr;
private MethodParameter paramNonSimpleType;
private MethodParameter returnParamNamedModelAttr;
-
private MethodParameter returnParamNonSimpleType;
- private ModelAndViewContainer mavContainer;
-
- private NativeWebRequest webRequest;
@Before
public void setUp() throws Exception {
- processor = new ModelAttributeMethodProcessor(false);
+ this.request = new ServletWebRequest(new MockHttpServletRequest());
+ this.container = new ModelAndViewContainer();
+ this.processor = new ModelAttributeMethodProcessor(false);
Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute",
- TestBean.class, Errors.class, int.class, TestBean.class, TestBean.class);
+ TestBean.class, Errors.class, int.class, TestBean.class,
+ TestBean.class, TestBean.class);
- paramNamedValidModelAttr = new MethodParameter(method, 0);
- paramErrors = new MethodParameter(method, 1);
- paramInt = new MethodParameter(method, 2);
- paramModelAttr = new MethodParameter(method, 3);
- paramNonSimpleType = new MethodParameter(method, 4);
+ this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0);
+ this.paramErrors = new SynthesizingMethodParameter(method, 1);
+ this.paramInt = new SynthesizingMethodParameter(method, 2);
+ this.paramModelAttr = new SynthesizingMethodParameter(method, 3);
+ this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4);
+ this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5);
- returnParamNamedModelAttr = new MethodParameter(getClass().getDeclaredMethod("annotatedReturnValue"), -1);
- returnParamNonSimpleType = new MethodParameter(getClass().getDeclaredMethod("notAnnotatedReturnValue"), -1);
+ method = getClass().getDeclaredMethod("annotatedReturnValue");
+ this.returnParamNamedModelAttr = new MethodParameter(method, -1);
- mavContainer = new ModelAndViewContainer();
-
- webRequest = new ServletWebRequest(new MockHttpServletRequest());
+ method = getClass().getDeclaredMethod("notAnnotatedReturnValue");
+ this.returnParamNonSimpleType = new MethodParameter(method, -1);
}
+
@Test
public void supportedParameters() throws Exception {
- // Only @ModelAttribute arguments
- assertTrue(processor.supportsParameter(paramNamedValidModelAttr));
- assertTrue(processor.supportsParameter(paramModelAttr));
+ assertTrue(this.processor.supportsParameter(this.paramNamedValidModelAttr));
+ assertTrue(this.processor.supportsParameter(this.paramModelAttr));
- assertFalse(processor.supportsParameter(paramErrors));
- assertFalse(processor.supportsParameter(paramInt));
- assertFalse(processor.supportsParameter(paramNonSimpleType));
+ assertFalse(this.processor.supportsParameter(this.paramErrors));
+ assertFalse(this.processor.supportsParameter(this.paramInt));
+ assertFalse(this.processor.supportsParameter(this.paramNonSimpleType));
}
@Test
@@ -108,135 +109,162 @@ public class ModelAttributeMethodProcessorTests {
processor = new ModelAttributeMethodProcessor(true);
// Only non-simple types, even if not annotated
- assertTrue(processor.supportsParameter(paramNamedValidModelAttr));
- assertTrue(processor.supportsParameter(paramErrors));
- assertTrue(processor.supportsParameter(paramModelAttr));
- assertTrue(processor.supportsParameter(paramNonSimpleType));
+ assertTrue(this.processor.supportsParameter(this.paramNamedValidModelAttr));
+ assertTrue(this.processor.supportsParameter(this.paramErrors));
+ assertTrue(this.processor.supportsParameter(this.paramModelAttr));
+ assertTrue(this.processor.supportsParameter(this.paramNonSimpleType));
- assertFalse(processor.supportsParameter(paramInt));
+ assertFalse(this.processor.supportsParameter(this.paramInt));
}
@Test
public void supportedReturnTypes() throws Exception {
processor = new ModelAttributeMethodProcessor(false);
- assertTrue(processor.supportsReturnType(returnParamNamedModelAttr));
- assertFalse(processor.supportsReturnType(returnParamNonSimpleType));
+ assertTrue(this.processor.supportsReturnType(returnParamNamedModelAttr));
+ assertFalse(this.processor.supportsReturnType(returnParamNonSimpleType));
}
@Test
public void supportedReturnTypesInDefaultResolutionMode() throws Exception {
processor = new ModelAttributeMethodProcessor(true);
- assertTrue(processor.supportsReturnType(returnParamNamedModelAttr));
- assertTrue(processor.supportsReturnType(returnParamNonSimpleType));
+ assertTrue(this.processor.supportsReturnType(returnParamNamedModelAttr));
+ assertTrue(this.processor.supportsReturnType(returnParamNonSimpleType));
}
@Test
public void bindExceptionRequired() throws Exception {
- assertTrue(processor.isBindExceptionRequired(null, paramNonSimpleType));
+ assertTrue(this.processor.isBindExceptionRequired(null, this.paramNonSimpleType));
+ assertFalse(this.processor.isBindExceptionRequired(null, this.paramNamedValidModelAttr));
}
@Test
- public void bindExceptionNotRequired() throws Exception {
- assertFalse(processor.isBindExceptionRequired(null, paramNamedValidModelAttr));
+ public void resolveArgumentFromModel() throws Exception {
+ testGetAttributeFromModel("attrName", this.paramNamedValidModelAttr);
+ testGetAttributeFromModel("testBean", this.paramModelAttr);
+ testGetAttributeFromModel("testBean", this.paramNonSimpleType);
}
@Test
- public void resovleArgumentFromModel() throws Exception {
- getAttributeFromModel("attrName", paramNamedValidModelAttr);
- getAttributeFromModel("testBean", paramModelAttr);
- getAttributeFromModel("testBean", paramNonSimpleType);
+ public void resovleArgumentViaDefaultConstructor() throws Exception {
+ WebDataBinder dataBinder = new WebRequestDataBinder(null);
+ WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
+ given(factory.createBinder(anyObject(), notNull(), eq("attrName"))).willReturn(dataBinder);
+
+ this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
+ verify(factory).createBinder(anyObject(), notNull(), eq("attrName"));
}
- private void getAttributeFromModel(String expectedAttributeName, MethodParameter param) throws Exception {
+ @Test
+ public void resolveArgumentValidation() throws Exception {
+ String name = "attrName";
Object target = new TestBean();
- mavContainer.addAttribute(expectedAttributeName, target);
+ this.container.addAttribute(name, target);
- WebDataBinder dataBinder = new WebRequestDataBinder(target);
+ StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
- given(factory.createBinder(webRequest, target, expectedAttributeName)).willReturn(dataBinder);
+ given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
- processor.resolveArgument(param, mavContainer, webRequest, factory);
- verify(factory).createBinder(webRequest, target, expectedAttributeName);
+ this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
+
+ assertTrue(dataBinder.isBindInvoked());
+ assertTrue(dataBinder.isValidateInvoked());
}
@Test
- public void resovleArgumentViaDefaultConstructor() throws Exception {
- WebDataBinder dataBinder = new WebRequestDataBinder(null);
+ public void resolveArgumentBindingDisabledPreviously() throws Exception {
+ String name = "attrName";
+ Object target = new TestBean();
+ this.container.addAttribute(name, target);
+
+ // Declare binding disabled (e.g. via @ModelAttribute method)
+ this.container.setBindingDisabled(name);
+ StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
- given(factory.createBinder((NativeWebRequest) anyObject(), notNull(), eq("attrName"))).willReturn(dataBinder);
+ given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
- processor.resolveArgument(paramNamedValidModelAttr, mavContainer, webRequest, factory);
+ this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
- verify(factory).createBinder((NativeWebRequest) anyObject(), notNull(), eq("attrName"));
+ assertFalse(dataBinder.isBindInvoked());
+ assertTrue(dataBinder.isValidateInvoked());
}
@Test
- public void resolveArgumentValidation() throws Exception {
- String name = "attrName";
+ public void resolveArgumentBindingDisabled() throws Exception {
+ String name = "noBindAttr";
Object target = new TestBean();
- mavContainer.addAttribute(name, target);
+ this.container.addAttribute(name, target);
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
- WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
- given(binderFactory.createBinder(webRequest, target, name)).willReturn(dataBinder);
+ WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
+ given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
- processor.resolveArgument(paramNamedValidModelAttr, mavContainer, webRequest, binderFactory);
+ this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory);
- assertTrue(dataBinder.isBindInvoked());
+ assertFalse(dataBinder.isBindInvoked());
assertTrue(dataBinder.isValidateInvoked());
}
@Test(expected = BindException.class)
- public void resovleArgumentBindException() throws Exception {
+ public void resolveArgumentBindException() throws Exception {
String name = "testBean";
Object target = new TestBean();
- mavContainer.getModel().addAttribute(target);
+ this.container.getModel().addAttribute(target);
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
dataBinder.getBindingResult().reject("error");
-
WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
- given(binderFactory.createBinder(webRequest, target, name)).willReturn(dataBinder);
+ given(binderFactory.createBinder(this.request, target, name)).willReturn(dataBinder);
- processor.resolveArgument(paramNonSimpleType, mavContainer, webRequest, binderFactory);
- verify(binderFactory).createBinder(webRequest, target, name);
+ this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory);
+ verify(binderFactory).createBinder(this.request, target, name);
}
@Test // SPR-9378
public void resolveArgumentOrdering() throws Exception {
String name = "testBean";
Object testBean = new TestBean(name);
- mavContainer.addAttribute(name, testBean);
- mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, testBean);
+ this.container.addAttribute(name, testBean);
+ this.container.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, testBean);
Object anotherTestBean = new TestBean();
- mavContainer.addAttribute("anotherTestBean", anotherTestBean);
+ this.container.addAttribute("anotherTestBean", anotherTestBean);
StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name);
WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
- given(binderFactory.createBinder(webRequest, testBean, name)).willReturn(dataBinder);
+ given(binderFactory.createBinder(this.request, testBean, name)).willReturn(dataBinder);
- processor.resolveArgument(paramModelAttr, mavContainer, webRequest, binderFactory);
+ this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory);
- assertSame("Resolved attribute should be updated to be last in the order",
- testBean, mavContainer.getModel().values().toArray()[1]);
- assertSame("BindingResult of resolved attribute should be last in the order",
- dataBinder.getBindingResult(), mavContainer.getModel().values().toArray()[2]);
+ Object[] values = this.container.getModel().values().toArray();
+ assertSame("Resolved attribute should be updated to be last", testBean, values[1]);
+ assertSame("BindingResult of resolved attr should be last", dataBinder.getBindingResult(), values[2]);
}
@Test
public void handleAnnotatedReturnValue() throws Exception {
- processor.handleReturnValue("expected", returnParamNamedModelAttr, mavContainer, webRequest);
- assertEquals("expected", mavContainer.getModel().get("modelAttrName"));
+ this.processor.handleReturnValue("expected", this.returnParamNamedModelAttr, this.container, this.request);
+ assertEquals("expected", this.container.getModel().get("modelAttrName"));
}
@Test
public void handleNotAnnotatedReturnValue() throws Exception {
TestBean testBean = new TestBean("expected");
- processor.handleReturnValue(testBean, returnParamNonSimpleType, mavContainer, webRequest);
+ this.processor.handleReturnValue(testBean, this.returnParamNonSimpleType, this.container, this.request);
+ assertSame(testBean, this.container.getModel().get("testBean"));
+ }
+
- assertSame(testBean, mavContainer.getModel().get("testBean"));
+ private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception {
+ Object target = new TestBean();
+ this.container.addAttribute(expectedAttrName, target);
+
+ WebDataBinder dataBinder = new WebRequestDataBinder(target);
+ WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
+ given(factory.createBinder(this.request, target, expectedAttrName)).willReturn(dataBinder);
+
+ this.processor.resolveArgument(param, this.container, this.request, factory);
+ verify(factory).createBinder(this.request, target, expectedAttrName);
}
@@ -246,6 +274,7 @@ public class ModelAttributeMethodProcessorTests {
private boolean validateInvoked;
+
public StubRequestDataBinder(Object target, String objectName) {
super(target, objectName);
}
@@ -285,13 +314,18 @@ public class ModelAttributeMethodProcessorTests {
private static class ModelAttributeHandler {
@SuppressWarnings("unused")
- public void modelAttribute(@ModelAttribute("attrName") @Valid TestBean annotatedAttr, Errors errors,
- int intArg, @ModelAttribute TestBean defaultNameAttr, TestBean notAnnotatedAttr) {
+ public void modelAttribute(
+ @ModelAttribute("attrName") @Valid TestBean annotatedAttr,
+ Errors errors,
+ int intArg,
+ @ModelAttribute TestBean defaultNameAttr,
+ @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr,
+ TestBean notAnnotatedAttr) {
}
}
- @ModelAttribute("modelAttrName")
+ @ModelAttribute("modelAttrName") @SuppressWarnings("unused")
private String annotatedReturnValue() {
return null;
}
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java
index e0a3c69f..24c61825 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,19 +16,12 @@
package org.springframework.web.method.annotation;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.BDDMockito.mock;
-
import java.lang.reflect.Method;
-import java.util.Arrays;
+import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
+
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.ui.Model;
@@ -43,10 +36,19 @@ import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.mock;
+
/**
* Text fixture for {@link ModelFactory} tests.
@@ -55,103 +57,116 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*/
public class ModelFactoryTests {
- private TestController controller = new TestController();
+ private NativeWebRequest webRequest;
- private InvocableHandlerMethod handleMethod;
+ private SessionAttributesHandler attributeHandler;
- private InvocableHandlerMethod handleSessionAttrMethod;
+ private SessionAttributeStore attributeStore;
- private SessionAttributesHandler sessionAttrsHandler;
+ private TestController controller = new TestController();
- private SessionAttributeStore sessionAttributeStore;
-
- private NativeWebRequest webRequest;
+ private ModelAndViewContainer mavContainer;
@Before
public void setUp() throws Exception {
- this.controller = new TestController();
-
- Method method = TestController.class.getDeclaredMethod("handle");
- this.handleMethod = new InvocableHandlerMethod(this.controller, method);
-
- method = TestController.class.getDeclaredMethod("handleSessionAttr", String.class);
- this.handleSessionAttrMethod = new InvocableHandlerMethod(this.controller, method);
-
- this.sessionAttributeStore = new DefaultSessionAttributeStore();
- this.sessionAttrsHandler = new SessionAttributesHandler(TestController.class, this.sessionAttributeStore);
this.webRequest = new ServletWebRequest(new MockHttpServletRequest());
+ this.attributeStore = new DefaultSessionAttributeStore();
+ this.attributeHandler = new SessionAttributesHandler(TestController.class, this.attributeStore);
+ this.controller = new TestController();
+ this.mavContainer = new ModelAndViewContainer();
}
@Test
public void modelAttributeMethod() throws Exception {
ModelFactory modelFactory = createModelFactory("modelAttr", Model.class);
- ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod);
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
- assertEquals(Boolean.TRUE, mavContainer.getModel().get("modelAttr"));
+ assertEquals(Boolean.TRUE, this.mavContainer.getModel().get("modelAttr"));
}
@Test
public void modelAttributeMethodWithExplicitName() throws Exception {
ModelFactory modelFactory = createModelFactory("modelAttrWithName");
- ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod);
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
- assertEquals(Boolean.TRUE, mavContainer.getModel().get("name"));
+ assertEquals(Boolean.TRUE, this.mavContainer.getModel().get("name"));
}
@Test
public void modelAttributeMethodWithNameByConvention() throws Exception {
ModelFactory modelFactory = createModelFactory("modelAttrConvention");
- ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod);
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
- assertEquals(Boolean.TRUE, mavContainer.getModel().get("boolean"));
+ assertEquals(Boolean.TRUE, this.mavContainer.getModel().get("boolean"));
}
@Test
public void modelAttributeMethodWithNullReturnValue() throws Exception {
ModelFactory modelFactory = createModelFactory("nullModelAttr");
- ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod);
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
- assertTrue(mavContainer.containsAttribute("name"));
- assertNull(mavContainer.getModel().get("name"));
+ assertTrue(this.mavContainer.containsAttribute("name"));
+ assertNull(this.mavContainer.getModel().get("name"));
}
@Test
- public void sessionAttribute() throws Exception {
- this.sessionAttributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
+ public void modelAttributeWithBindingDisabled() throws Exception {
+ ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled");
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
+
+ assertTrue(this.mavContainer.containsAttribute("foo"));
+ assertTrue(this.mavContainer.isBindingDisabled("foo"));
+ }
+
+ @Test
+ public void modelAttributeFromSessionWithBindingDisabled() throws Exception {
+ Foo foo = new Foo();
+ this.attributeStore.storeAttribute(this.webRequest, "foo", foo);
- // Resolve successfully handler session attribute once
- assertTrue(sessionAttrsHandler.isHandlerSessionAttribute("sessionAttr", null));
+ ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled");
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
+
+ assertTrue(this.mavContainer.containsAttribute("foo"));
+ assertSame(foo, this.mavContainer.getModel().get("foo"));
+ assertTrue(this.mavContainer.isBindingDisabled("foo"));
+ }
+
+ @Test
+ public void sessionAttribute() throws Exception {
+ this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
ModelFactory modelFactory = createModelFactory("modelAttr", Model.class);
- ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod);
+ HandlerMethod handlerMethod = createHandlerMethod("handle");
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
- assertEquals("sessionAttrValue", mavContainer.getModel().get("sessionAttr"));
+ assertEquals("sessionAttrValue", this.mavContainer.getModel().get("sessionAttr"));
}
@Test
public void sessionAttributeNotPresent() throws Exception {
- ModelFactory modelFactory = new ModelFactory(null, null, this.sessionAttrsHandler);
-
+ ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler);
+ HandlerMethod handlerMethod = createHandlerMethod("handleSessionAttr", String.class);
try {
- modelFactory.initModel(this.webRequest, new ModelAndViewContainer(), this.handleSessionAttrMethod);
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
fail("Expected HttpSessionRequiredException");
}
catch (HttpSessionRequiredException e) {
// expected
}
- this.sessionAttributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
- ModelAndViewContainer mavContainer = new ModelAndViewContainer();
- modelFactory.initModel(this.webRequest, mavContainer, this.handleSessionAttrMethod);
+ // Now add attribute and try again
+ this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
- assertEquals("sessionAttrValue", mavContainer.getModel().get("sessionAttr"));
+ modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
+ assertEquals("sessionAttrValue", this.mavContainer.getModel().get("sessionAttr"));
}
@Test
@@ -165,11 +180,12 @@ public class ModelFactoryTests {
WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
given(binderFactory.createBinder(this.webRequest, command, commandName)).willReturn(dataBinder);
- ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler);
+ ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler);
modelFactory.updateModel(this.webRequest, container);
assertEquals(command, container.getModel().get(commandName));
- assertSame(dataBinder.getBindingResult(), container.getModel().get(bindingResultKey(commandName)));
+ String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + commandName;
+ assertSame(dataBinder.getBindingResult(), container.getModel().get(bindingResultKey));
assertEquals(2, container.getModel().size());
}
@@ -184,11 +200,11 @@ public class ModelFactoryTests {
WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
given(binderFactory.createBinder(this.webRequest, attribute, attributeName)).willReturn(dataBinder);
- ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler);
+ ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler);
modelFactory.updateModel(this.webRequest, container);
assertEquals(attribute, container.getModel().get(attributeName));
- assertEquals(attribute, this.sessionAttributeStore.retrieveAttribute(this.webRequest, attributeName));
+ assertEquals(attribute, this.attributeStore.retrieveAttribute(this.webRequest, attributeName));
}
@Test
@@ -198,9 +214,7 @@ public class ModelFactoryTests {
ModelAndViewContainer container = new ModelAndViewContainer();
container.addAttribute(attributeName, attribute);
- // Store and resolve once (to be "remembered")
- this.sessionAttributeStore.storeAttribute(this.webRequest, attributeName, attribute);
- this.sessionAttrsHandler.isHandlerSessionAttribute(attributeName, null);
+ this.attributeStore.storeAttribute(this.webRequest, attributeName, attribute);
WebDataBinder dataBinder = new WebDataBinder(attribute, attributeName);
WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
@@ -208,11 +222,11 @@ public class ModelFactoryTests {
container.getSessionStatus().setComplete();
- ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler);
+ ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler);
modelFactory.updateModel(this.webRequest, container);
assertEquals(attribute, container.getModel().get(attributeName));
- assertNull(this.sessionAttributeStore.retrieveAttribute(this.webRequest, attributeName));
+ assertNull(this.attributeStore.retrieveAttribute(this.webRequest, attributeName));
}
// SPR-12542
@@ -233,34 +247,34 @@ public class ModelFactoryTests {
WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class);
given(binderFactory.createBinder(this.webRequest, attribute, attributeName)).willReturn(dataBinder);
- ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler);
+ ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler);
modelFactory.updateModel(this.webRequest, container);
assertEquals(queryParam, container.getModel().get(queryParamName));
assertEquals(1, container.getModel().size());
- assertEquals(attribute, this.sessionAttributeStore.retrieveAttribute(this.webRequest, attributeName));
+ assertEquals(attribute, this.attributeStore.retrieveAttribute(this.webRequest, attributeName));
}
- private String bindingResultKey(String key) {
- return BindingResult.MODEL_KEY_PREFIX + key;
- }
-
- private ModelFactory createModelFactory(String methodName, Class<?>... parameterTypes) throws Exception{
- Method method = TestController.class.getMethod(methodName, parameterTypes);
+ private ModelFactory createModelFactory(String methodName, Class<?>... parameterTypes) throws Exception {
+ HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
+ resolvers.addResolver(new ModelMethodProcessor());
- HandlerMethodArgumentResolverComposite argResolvers = new HandlerMethodArgumentResolverComposite();
- argResolvers.addResolver(new ModelMethodProcessor());
+ InvocableHandlerMethod modelMethod = createHandlerMethod(methodName, parameterTypes);
+ modelMethod.setHandlerMethodArgumentResolvers(resolvers);
+ modelMethod.setDataBinderFactory(null);
+ modelMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer());
- InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(this.controller, method);
- handlerMethod.setHandlerMethodArgumentResolvers(argResolvers);
- handlerMethod.setDataBinderFactory(null);
- handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer());
+ return new ModelFactory(Collections.singletonList(modelMethod), null, this.attributeHandler);
+ }
- return new ModelFactory(Arrays.asList(handlerMethod), null, this.sessionAttrsHandler);
+ private InvocableHandlerMethod createHandlerMethod(String methodName, Class<?>... paramTypes) throws Exception {
+ Method method = this.controller.getClass().getMethod(methodName, paramTypes);
+ return new InvocableHandlerMethod(this.controller, method);
}
- @SessionAttributes("sessionAttr") @SuppressWarnings("unused")
+
+ @SessionAttributes({"sessionAttr", "foo"}) @SuppressWarnings("unused")
private static class TestController {
@ModelAttribute
@@ -283,6 +297,11 @@ public class ModelFactoryTests {
return null;
}
+ @ModelAttribute(name="foo", binding=false)
+ public Foo modelAttrWithBindingDisabled() {
+ return new Foo();
+ }
+
public void handle() {
}
@@ -290,4 +309,7 @@ public class ModelFactoryTests {
}
}
+ private static class Foo {
+ }
+
}
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java
index b484e2ff..a453abb0 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -17,6 +17,9 @@
package org.springframework.web.method.annotation;
import java.lang.reflect.Method;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
import java.util.Map;
import org.junit.After;
@@ -25,10 +28,14 @@ import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
+import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
+import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
+import org.springframework.web.bind.support.DefaultDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletWebRequest;
@@ -50,7 +57,11 @@ public class RequestHeaderMethodArgumentResolverTests {
private MethodParameter paramNamedValueStringArray;
private MethodParameter paramSystemProperty;
private MethodParameter paramContextPath;
+ private MethodParameter paramResolvedNameWithExpression;
+ private MethodParameter paramResolvedNameWithPlaceholder;
private MethodParameter paramNamedValueMap;
+ private MethodParameter paramDate;
+ private MethodParameter paramInstant;
private MockHttpServletRequest servletRequest;
@@ -64,12 +75,16 @@ public class RequestHeaderMethodArgumentResolverTests {
context.refresh();
resolver = new RequestHeaderMethodArgumentResolver(context.getBeanFactory());
- Method method = getClass().getMethod("params", String.class, String[].class, String.class, String.class, Map.class);
+ Method method = ReflectionUtils.findMethod(getClass(), "params", (Class<?>[]) null);
paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 0);
paramNamedValueStringArray = new SynthesizingMethodParameter(method, 1);
paramSystemProperty = new SynthesizingMethodParameter(method, 2);
paramContextPath = new SynthesizingMethodParameter(method, 3);
- paramNamedValueMap = new SynthesizingMethodParameter(method, 4);
+ paramResolvedNameWithExpression = new SynthesizingMethodParameter(method, 4);
+ paramResolvedNameWithPlaceholder = new SynthesizingMethodParameter(method, 5);
+ paramNamedValueMap = new SynthesizingMethodParameter(method, 6);
+ paramDate = new SynthesizingMethodParameter(method, 7);
+ paramInstant = new SynthesizingMethodParameter(method, 8);
servletRequest = new MockHttpServletRequest();
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
@@ -97,45 +112,77 @@ public class RequestHeaderMethodArgumentResolverTests {
servletRequest.addHeader("name", expected);
Object result = resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, webRequest, null);
-
assertTrue(result instanceof String);
- assertEquals("Invalid result", expected, result);
+ assertEquals(expected, result);
}
@Test
public void resolveStringArrayArgument() throws Exception {
- String[] expected = new String[]{"foo", "bar"};
+ String[] expected = new String[] {"foo", "bar"};
servletRequest.addHeader("name", expected);
Object result = resolver.resolveArgument(paramNamedValueStringArray, null, webRequest, null);
-
assertTrue(result instanceof String[]);
- assertArrayEquals("Invalid result", expected, (String[]) result);
+ assertArrayEquals(expected, (String[]) result);
}
@Test
public void resolveDefaultValue() throws Exception {
Object result = resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, webRequest, null);
-
assertTrue(result instanceof String);
- assertEquals("Invalid result", "bar", result);
+ assertEquals("bar", result);
}
@Test
public void resolveDefaultValueFromSystemProperty() throws Exception {
System.setProperty("systemProperty", "bar");
- Object result = resolver.resolveArgument(paramSystemProperty, null, webRequest, null);
- System.clearProperty("systemProperty");
+ try {
+ Object result = resolver.resolveArgument(paramSystemProperty, null, webRequest, null);
+ assertTrue(result instanceof String);
+ assertEquals("bar", result);
+ }
+ finally {
+ System.clearProperty("systemProperty");
+ }
+ }
- assertTrue(result instanceof String);
- assertEquals("bar", result);
+ @Test
+ public void resolveNameFromSystemPropertyThroughExpression() throws Exception {
+ String expected = "foo";
+ servletRequest.addHeader("bar", expected);
+
+ System.setProperty("systemProperty", "bar");
+ try {
+ Object result = resolver.resolveArgument(paramResolvedNameWithExpression, null, webRequest, null);
+ assertTrue(result instanceof String);
+ assertEquals(expected, result);
+ }
+ finally {
+ System.clearProperty("systemProperty");
+ }
+ }
+
+ @Test
+ public void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception {
+ String expected = "foo";
+ servletRequest.addHeader("bar", expected);
+
+ System.setProperty("systemProperty", "bar");
+ try {
+ Object result = resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null);
+ assertTrue(result instanceof String);
+ assertEquals(expected, result);
+ }
+ finally {
+ System.clearProperty("systemProperty");
+ }
}
@Test
public void resolveDefaultValueFromRequest() throws Exception {
servletRequest.setContextPath("/bar");
- Object result = resolver.resolveArgument(paramContextPath, null, webRequest, null);
+ Object result = resolver.resolveArgument(paramContextPath, null, webRequest, null);
assertTrue(result instanceof String);
assertEquals("/bar", result);
}
@@ -145,12 +192,46 @@ public class RequestHeaderMethodArgumentResolverTests {
resolver.resolveArgument(paramNamedValueStringArray, null, webRequest, null);
}
+ @Test
+ @SuppressWarnings("deprecation")
+ public void dateConversion() throws Exception {
+ String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100";
+ servletRequest.addHeader("name", rfc1123val);
+
+ ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
+ bindingInitializer.setConversionService(new DefaultFormattingConversionService());
+ Object result = resolver.resolveArgument(paramDate, null, webRequest,
+ new DefaultDataBinderFactory(bindingInitializer));
+
+ assertTrue(result instanceof Date);
+ assertEquals(new Date(rfc1123val), result);
+ }
+
+ @Test
+ public void instantConversion() throws Exception {
+ String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100";
+ servletRequest.addHeader("name", rfc1123val);
+
+ ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
+ bindingInitializer.setConversionService(new DefaultFormattingConversionService());
+ Object result = resolver.resolveArgument(paramInstant, null, webRequest,
+ new DefaultDataBinderFactory(bindingInitializer));
+
+ assertTrue(result instanceof Instant);
+ assertEquals(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val)), result);
+ }
+
- public void params(@RequestHeader(name = "name", defaultValue = "bar") String param1,
- @RequestHeader("name") String[] param2,
- @RequestHeader(name = "name", defaultValue="#{systemProperties.systemProperty}") String param3,
- @RequestHeader(name = "name", defaultValue="#{request.contextPath}") String param4,
- @RequestHeader("name") Map<?, ?> unsupported) {
+ public void params(
+ @RequestHeader(name = "name", defaultValue = "bar") String param1,
+ @RequestHeader("name") String[] param2,
+ @RequestHeader(name = "name", defaultValue="#{systemProperties.systemProperty}") String param3,
+ @RequestHeader(name = "name", defaultValue="#{request.contextPath}") String param4,
+ @RequestHeader("#{systemProperties.systemProperty}") String param5,
+ @RequestHeader("${systemProperty}") String param6,
+ @RequestHeader("name") Map<?, ?> unsupported,
+ @RequestHeader("name") Date dateParam,
+ @RequestHeader("name") Instant instantParam) {
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java
index 00b61b5f..f77ce2bc 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java
@@ -37,6 +37,7 @@ import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockMultipartFile;
import org.springframework.mock.web.test.MockMultipartHttpServletRequest;
import org.springframework.mock.web.test.MockPart;
+import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestParam;
@@ -49,6 +50,7 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.support.MissingServletRequestPartException;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
@@ -82,6 +84,7 @@ public class RequestParamMethodArgumentResolverTests {
private MethodParameter paramRequired;
private MethodParameter paramNotRequired;
private MethodParameter paramOptional;
+ private MethodParameter multipartFileOptional;
private NativeWebRequest webRequest;
@@ -92,12 +95,7 @@ public class RequestParamMethodArgumentResolverTests {
public void setUp() throws Exception {
resolver = new RequestParamMethodArgumentResolver(null, true);
ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
-
- Method method = getClass().getMethod("params", String.class, String[].class,
- Map.class, MultipartFile.class, List.class, MultipartFile[].class,
- Part.class, List.class, Part[].class, Map.class,
- String.class, MultipartFile.class, List.class, Part.class,
- MultipartFile.class, String.class, String.class, Optional.class);
+ Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class<?>[]) null);
paramNamedDefaultValueString = new SynthesizingMethodParameter(method, 0);
paramNamedStringArray = new SynthesizingMethodParameter(method, 1);
@@ -121,6 +119,7 @@ public class RequestParamMethodArgumentResolverTests {
paramRequired = new SynthesizingMethodParameter(method, 15);
paramNotRequired = new SynthesizingMethodParameter(method, 16);
paramOptional = new SynthesizingMethodParameter(method, 17);
+ multipartFileOptional = new SynthesizingMethodParameter(method, 18);
request = new MockHttpServletRequest();
webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
@@ -130,19 +129,25 @@ public class RequestParamMethodArgumentResolverTests {
@Test
public void supportsParameter() {
resolver = new RequestParamMethodArgumentResolver(null, true);
- assertTrue("String parameter not supported", resolver.supportsParameter(paramNamedDefaultValueString));
- assertTrue("String array parameter not supported", resolver.supportsParameter(paramNamedStringArray));
- assertTrue("Named map not parameter supported", resolver.supportsParameter(paramNamedMap));
- assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFile));
- assertTrue("List<MultipartFile> parameter not supported", resolver.supportsParameter(paramMultipartFileList));
- assertTrue("MultipartFile[] parameter not supported", resolver.supportsParameter(paramMultipartFileArray));
- assertTrue("Part parameter not supported", resolver.supportsParameter(paramPart));
- assertTrue("List<Part> parameter not supported", resolver.supportsParameter(paramPartList));
- assertTrue("Part[] parameter not supported", resolver.supportsParameter(paramPartArray));
- assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(paramMap));
- assertTrue("Simple type params supported w/o annotations", resolver.supportsParameter(paramStringNotAnnot));
- assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFileNotAnnot));
- assertTrue("Part parameter not supported", resolver.supportsParameter(paramPartNotAnnot));
+ assertTrue(resolver.supportsParameter(paramNamedDefaultValueString));
+ assertTrue(resolver.supportsParameter(paramNamedStringArray));
+ assertTrue(resolver.supportsParameter(paramNamedMap));
+ assertTrue(resolver.supportsParameter(paramMultipartFile));
+ assertTrue(resolver.supportsParameter(paramMultipartFileList));
+ assertTrue(resolver.supportsParameter(paramMultipartFileArray));
+ assertTrue(resolver.supportsParameter(paramPart));
+ assertTrue(resolver.supportsParameter(paramPartList));
+ assertTrue(resolver.supportsParameter(paramPartArray));
+ assertFalse(resolver.supportsParameter(paramMap));
+ assertTrue(resolver.supportsParameter(paramStringNotAnnot));
+ assertTrue(resolver.supportsParameter(paramMultipartFileNotAnnot));
+ assertTrue(resolver.supportsParameter(paramMultipartFileListNotAnnot));
+ assertTrue(resolver.supportsParameter(paramPartNotAnnot));
+ assertFalse(resolver.supportsParameter(paramRequestPartAnnot));
+ assertTrue(resolver.supportsParameter(paramRequired));
+ assertTrue(resolver.supportsParameter(paramNotRequired));
+ assertTrue(resolver.supportsParameter(paramOptional));
+ assertTrue(resolver.supportsParameter(multipartFileOptional));
resolver = new RequestParamMethodArgumentResolver(null, false);
assertFalse(resolver.supportsParameter(paramStringNotAnnot));
@@ -188,6 +193,7 @@ public class RequestParamMethodArgumentResolverTests {
MultipartFile expected2 = new MockMultipartFile("mfilelist", "Hello World 2".getBytes());
request.addFile(expected1);
request.addFile(expected2);
+ request.addFile(new MockMultipartFile("other", "Hello World 3".getBytes()));
webRequest = new ServletWebRequest(request);
Object result = resolver.resolveArgument(paramMultipartFileList, null, webRequest, null);
@@ -202,6 +208,7 @@ public class RequestParamMethodArgumentResolverTests {
MultipartFile expected2 = new MockMultipartFile("mfilearray", "Hello World 2".getBytes());
request.addFile(expected1);
request.addFile(expected2);
+ request.addFile(new MockMultipartFile("other", "Hello World 3".getBytes()));
webRequest = new ServletWebRequest(request);
Object result = resolver.resolveArgument(paramMultipartFileArray, null, webRequest, null);
@@ -222,7 +229,6 @@ public class RequestParamMethodArgumentResolverTests {
webRequest = new ServletWebRequest(request);
Object result = resolver.resolveArgument(paramPart, null, webRequest, null);
-
assertTrue(result instanceof Part);
assertEquals("Invalid result", expected, result);
}
@@ -230,12 +236,13 @@ public class RequestParamMethodArgumentResolverTests {
@Test
public void resolvePartList() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
- MockPart expected1 = new MockPart("pfilelist", "Hello World 1".getBytes());
- MockPart expected2 = new MockPart("pfilelist", "Hello World 2".getBytes());
request.setMethod("POST");
request.setContentType("multipart/form-data");
+ MockPart expected1 = new MockPart("pfilelist", "Hello World 1".getBytes());
+ MockPart expected2 = new MockPart("pfilelist", "Hello World 2".getBytes());
request.addPart(expected1);
request.addPart(expected2);
+ request.addPart(new MockPart("other", "Hello World 3".getBytes()));
webRequest = new ServletWebRequest(request);
Object result = resolver.resolveArgument(paramPartList, null, webRequest, null);
@@ -252,6 +259,7 @@ public class RequestParamMethodArgumentResolverTests {
request.setContentType("multipart/form-data");
request.addPart(expected1);
request.addPart(expected2);
+ request.addPart(new MockPart("other", "Hello World 3".getBytes()));
webRequest = new ServletWebRequest(request);
Object result = resolver.resolveArgument(paramPartArray, null, webRequest, null);
@@ -307,12 +315,19 @@ public class RequestParamMethodArgumentResolverTests {
assertEquals(expected, ((List<?>) actual).get(0));
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = MultipartException.class)
+ public void noMultipartContent() throws Exception {
+ request.setMethod("POST");
+ resolver.resolveArgument(paramMultipartFile, null, webRequest, null);
+ fail("Expected exception: no multipart content");
+ }
+
+ @Test(expected = MissingServletRequestPartException.class)
public void missingMultipartFile() throws Exception {
request.setMethod("POST");
request.setContentType("multipart/form-data");
resolver.resolveArgument(paramMultipartFile, null, webRequest, null);
- fail("Expected exception: request is not MultiPartHttpServletRequest but param is MultipartFile");
+ fail("Expected exception: no such part found");
}
@Test
@@ -422,8 +437,45 @@ public class RequestParamMethodArgumentResolverTests {
assertEquals(123, ((Optional) result).get());
}
+ @Test
+ public void resolveOptionalMultipartFile() throws Exception {
+ ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
+ initializer.setConversionService(new DefaultConversionService());
+ WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer);
+
+ MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest();
+ MultipartFile expected = new MockMultipartFile("mfile", "Hello World".getBytes());
+ request.addFile(expected);
+ webRequest = new ServletWebRequest(request);
+
+ Object result = resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory);
+ assertTrue(result instanceof Optional);
+ assertEquals("Invalid result", expected, ((Optional<?>) result).get());
+ }
+
+ @Test
+ public void missingOptionalMultipartFile() throws Exception {
+ ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
+ initializer.setConversionService(new DefaultConversionService());
+ WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer);
+
+ request.setMethod("POST");
+ request.setContentType("multipart/form-data");
+ assertEquals(Optional.empty(), resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory));
+ }
+
+ @Test
+ public void optionalMultipartFileWithoutMultipartRequest() throws Exception {
+ ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
+ initializer.setConversionService(new DefaultConversionService());
+ WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer);
+
+ assertEquals(Optional.empty(), resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory));
+ }
+
- public void params(@RequestParam(name = "name", defaultValue = "bar") String param1,
+ public void handle(
+ @RequestParam(name = "name", defaultValue = "bar") String param1,
@RequestParam("name") String[] param2,
@RequestParam("name") Map<?, ?> param3,
@RequestParam("mfile") MultipartFile param4,
@@ -440,7 +492,8 @@ public class RequestParamMethodArgumentResolverTests {
@RequestPart MultipartFile requestPartAnnot,
@RequestParam("name") String paramRequired,
@RequestParam(name = "name", required = false) String paramNotRequired,
- @RequestParam("name") Optional<Integer> paramOptional) {
+ @RequestParam("name") Optional<Integer> paramOptional,
+ @RequestParam("mfile") Optional<MultipartFile> multipartFileOptional) {
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java
index 1642351e..09f3700a 100644
--- a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -15,74 +15,135 @@
*/
package org.springframework.web.util;
-import static org.junit.Assert.assertEquals;
-
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
-import org.junit.Before;
import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
/**
* Unit tests for {@link DefaultUriTemplateHandler}.
+ *
* @author Rossen Stoyanchev
*/
public class DefaultUriTemplateHandlerTests {
- private DefaultUriTemplateHandler handler;
-
-
- @Before
- public void setUp() throws Exception {
- this.handler = new DefaultUriTemplateHandler();
- }
+ private final DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
@Test
- public void baseUrl() throws Exception {
+ public void baseUrlWithoutPath() throws Exception {
this.handler.setBaseUrl("http://localhost:8080");
URI actual = this.handler.expand("/myapiresource");
- URI expected = new URI("http://localhost:8080/myapiresource");
- assertEquals(expected, actual);
+ assertEquals("http://localhost:8080/myapiresource", actual.toString());
}
@Test
- public void baseUrlWithPartialPath() throws Exception {
+ public void baseUrlWithPath() throws Exception {
this.handler.setBaseUrl("http://localhost:8080/context");
URI actual = this.handler.expand("/myapiresource");
- URI expected = new URI("http://localhost:8080/context/myapiresource");
- assertEquals(expected, actual);
+ assertEquals("http://localhost:8080/context/myapiresource", actual.toString());
+ }
+
+ @Test // SPR-14147
+ public void defaultUriVariables() throws Exception {
+ Map<String, String> defaultVars = new HashMap<>(2);
+ defaultVars.put("host", "api.example.com");
+ defaultVars.put("port", "443");
+ this.handler.setDefaultUriVariables(defaultVars);
+
+ Map<String, Object> vars = new HashMap<>(1);
+ vars.put("id", 123L);
+
+ String template = "https://{host}:{port}/v42/customers/{id}";
+ URI actual = this.handler.expand(template, vars);
+
+ assertEquals("https://api.example.com:443/v42/customers/123", actual.toString());
}
@Test
- public void expandWithFullPath() throws Exception {
- Map<String, String> vars = new HashMap<String, String>(2);
+ public void parsePathIsOff() throws Exception {
+ this.handler.setParsePath(false);
+ Map<String, String> vars = new HashMap<>(2);
vars.put("hotel", "1");
vars.put("publicpath", "pics/logo.png");
String template = "http://example.com/hotels/{hotel}/pic/{publicpath}";
-
URI actual = this.handler.expand(template, vars);
- URI expected = new URI("http://example.com/hotels/1/pic/pics/logo.png");
- assertEquals(expected, actual);
+ assertEquals("http://example.com/hotels/1/pic/pics/logo.png", actual.toString());
}
@Test
- public void expandWithFullPathAndParsePathEnabled() throws Exception {
- Map<String, String> vars = new HashMap<String, String>(2);
+ public void parsePathIsOn() throws Exception {
+ this.handler.setParsePath(true);
+ Map<String, String> vars = new HashMap<>(2);
vars.put("hotel", "1");
vars.put("publicpath", "pics/logo.png");
vars.put("scale", "150x150");
String template = "http://example.com/hotels/{hotel}/pic/{publicpath}/size/{scale}";
+ URI actual = this.handler.expand(template, vars);
- this.handler.setParsePath(true);
+ assertEquals("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150", actual.toString());
+ }
+
+ @Test
+ public void strictEncodingIsOffWithMap() throws Exception {
+ this.handler.setStrictEncoding(false);
+ Map<String, String> vars = new HashMap<>(2);
+ vars.put("userId", "john;doe");
+ String template = "http://www.example.com/user/{userId}/dashboard";
+ URI actual = this.handler.expand(template, vars);
+
+ assertEquals("http://www.example.com/user/john;doe/dashboard", actual.toString());
+ }
+
+ @Test
+ public void strictEncodingOffWithArray() throws Exception {
+ this.handler.setStrictEncoding(false);
+ String template = "http://www.example.com/user/{userId}/dashboard";
+ URI actual = this.handler.expand(template, "john;doe");
+
+ assertEquals("http://www.example.com/user/john;doe/dashboard", actual.toString());
+ }
+
+ @Test
+ public void strictEncodingOnWithMap() throws Exception {
+ this.handler.setStrictEncoding(true);
+ Map<String, String> vars = new HashMap<>(2);
+ vars.put("userId", "john;doe");
+ String template = "http://www.example.com/user/{userId}/dashboard";
+ URI actual = this.handler.expand(template, vars);
+
+ assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", actual.toString());
+ }
+
+ @Test
+ public void strictEncodingOnWithArray() throws Exception {
+ this.handler.setStrictEncoding(true);
+ String template = "http://www.example.com/user/{userId}/dashboard";
+ URI actual = this.handler.expand(template, "john;doe");
+
+ assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", actual.toString());
+ }
+
+ @Test // SPR-14147
+ public void strictEncodingAndDefaultUriVariables() throws Exception {
+ Map<String, String> defaultVars = new HashMap<>(1);
+ defaultVars.put("host", "www.example.com");
+ this.handler.setDefaultUriVariables(defaultVars);
+ this.handler.setStrictEncoding(true);
+
+ Map<String, Object> vars = new HashMap<>(1);
+ vars.put("userId", "john;doe");
+
+ String template = "http://{host}/user/{userId}/dashboard";
URI actual = this.handler.expand(template, vars);
- URI expected = new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150");
- assertEquals(expected, actual);
+ assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", actual.toString());
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java b/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java
index 5001f9fb..c32405e4 100644
--- a/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java
@@ -101,7 +101,8 @@ public class Log4jWebConfigurerTests {
try {
assertLogOutput();
- } finally {
+ }
+ finally {
Log4jWebConfigurer.shutdownLogging(sc);
}
assertTrue(MockLog4jAppender.closeCalled);
@@ -132,7 +133,8 @@ public class Log4jWebConfigurerTests {
try {
assertLogOutput();
- } finally {
+ }
+ finally {
listener.contextDestroyed(new ServletContextEvent(sc));
}
assertTrue(MockLog4jAppender.closeCalled);
diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java
index a047af03..99c642f0 100644
--- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -30,116 +30,126 @@ import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
*/
public class UriTemplateTests {
@Test
public void getVariableNames() throws Exception {
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
List<String> variableNames = template.getVariableNames();
assertEquals("Invalid variable names", Arrays.asList("hotel", "booking"), variableNames);
}
@Test
public void expandVarArgs() throws Exception {
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
URI result = template.expand("1", "42");
- assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result);
+ assertEquals("Invalid expanded template", new URI("/hotels/1/bookings/42"), result);
+ }
+
+ // SPR-9712
+
+ @Test
+ public void expandVarArgsWithArrayValue() throws Exception {
+ UriTemplate template = new UriTemplate("/sum?numbers={numbers}");
+ URI result = template.expand(new int[] {1, 2, 3});
+ assertEquals(new URI("/sum?numbers=1,2,3"), result);
}
@Test(expected = IllegalArgumentException.class)
public void expandVarArgsNotEnoughVariables() throws Exception {
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
template.expand("1");
}
@Test
public void expandMap() throws Exception {
- Map<String, String> uriVariables = new HashMap<String, String>(2);
+ Map<String, String> uriVariables = new HashMap<>(2);
uriVariables.put("booking", "42");
uriVariables.put("hotel", "1");
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
URI result = template.expand(uriVariables);
- assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result);
+ assertEquals("Invalid expanded template", new URI("/hotels/1/bookings/42"), result);
}
@Test
public void expandMapDuplicateVariables() throws Exception {
UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}");
- assertEquals("Invalid variable names", Arrays.asList("c", "c", "c"), template.getVariableNames());
+ assertEquals(Arrays.asList("c", "c", "c"), template.getVariableNames());
URI result = template.expand(Collections.singletonMap("c", "cheeseburger"));
- assertEquals("Invalid expanded template", new URI("/order/cheeseburger/cheeseburger/cheeseburger"), result);
+ assertEquals(new URI("/order/cheeseburger/cheeseburger/cheeseburger"), result);
}
@Test
public void expandMapNonString() throws Exception {
- Map<String, Integer> uriVariables = new HashMap<String, Integer>(2);
+ Map<String, Integer> uriVariables = new HashMap<>(2);
uriVariables.put("booking", 42);
uriVariables.put("hotel", 1);
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
URI result = template.expand(uriVariables);
- assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result);
+ assertEquals("Invalid expanded template", new URI("/hotels/1/bookings/42"), result);
}
@Test
public void expandMapEncoded() throws Exception {
Map<String, String> uriVariables = Collections.singletonMap("hotel", "Z\u00fcrich");
- UriTemplate template = new UriTemplate("http://example.com/hotel list/{hotel}");
+ UriTemplate template = new UriTemplate("/hotel list/{hotel}");
URI result = template.expand(uriVariables);
- assertEquals("Invalid expanded template", new URI("http://example.com/hotel%20list/Z%C3%BCrich"), result);
+ assertEquals("Invalid expanded template", new URI("/hotel%20list/Z%C3%BCrich"), result);
}
@Test(expected = IllegalArgumentException.class)
public void expandMapUnboundVariables() throws Exception {
- Map<String, String> uriVariables = new HashMap<String, String>(2);
+ Map<String, String> uriVariables = new HashMap<>(2);
uriVariables.put("booking", "42");
uriVariables.put("bar", "1");
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
template.expand(uriVariables);
}
@Test
public void expandEncoded() throws Exception {
- UriTemplate template = new UriTemplate("http://example.com/hotel list/{hotel}");
+ UriTemplate template = new UriTemplate("/hotel list/{hotel}");
URI result = template.expand("Z\u00fcrich");
- assertEquals("Invalid expanded template", new URI("http://example.com/hotel%20list/Z%C3%BCrich"), result);
+ assertEquals("Invalid expanded template", new URI("/hotel%20list/Z%C3%BCrich"), result);
}
@Test
public void matches() throws Exception {
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
- assertTrue("UriTemplate does not match", template.matches("http://example.com/hotels/1/bookings/42"));
- assertFalse("UriTemplate matches", template.matches("http://example.com/hotels/bookings"));
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
+ assertTrue("UriTemplate does not match", template.matches("/hotels/1/bookings/42"));
+ assertFalse("UriTemplate matches", template.matches("/hotels/bookings"));
assertFalse("UriTemplate matches", template.matches(""));
assertFalse("UriTemplate matches", template.matches(null));
}
@Test
public void matchesCustomRegex() throws Exception {
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel:\\d+}");
- assertTrue("UriTemplate does not match", template.matches("http://example.com/hotels/42"));
- assertFalse("UriTemplate matches", template.matches("http://example.com/hotels/foo"));
+ UriTemplate template = new UriTemplate("/hotels/{hotel:\\d+}");
+ assertTrue("UriTemplate does not match", template.matches("/hotels/42"));
+ assertFalse("UriTemplate matches", template.matches("/hotels/foo"));
}
@Test
public void match() throws Exception {
- Map<String, String> expected = new HashMap<String, String>(2);
+ Map<String, String> expected = new HashMap<>(2);
expected.put("booking", "42");
expected.put("hotel", "1");
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}");
- Map<String, String> result = template.match("http://example.com/hotels/1/bookings/42");
+ UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
+ Map<String, String> result = template.match("/hotels/1/bookings/42");
assertEquals("Invalid match", expected, result);
}
@Test
public void matchCustomRegex() throws Exception {
- Map<String, String> expected = new HashMap<String, String>(2);
+ Map<String, String> expected = new HashMap<>(2);
expected.put("booking", "42");
expected.put("hotel", "1");
- UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel:\\d}/bookings/{booking:\\d+}");
- Map<String, String> result = template.match("http://example.com/hotels/1/bookings/42");
+ UriTemplate template = new UriTemplate("/hotels/{hotel:\\d}/bookings/{booking:\\d+}");
+ Map<String, String> result = template.match("/hotels/1/bookings/42");
assertEquals("Invalid match", expected, result);
}
@@ -164,7 +174,7 @@ public class UriTemplateTests {
public void matchMultipleInOneSegment() throws Exception {
UriTemplate template = new UriTemplate("/{foo}-{bar}");
Map<String, String> result = template.match("/12-34");
- Map<String, String> expected = new HashMap<String, String>(2);
+ Map<String, String> expected = new HashMap<>(2);
expected.put("foo", "12");
expected.put("bar", "34");
assertEquals("Invalid match", expected, result);
diff --git a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java
index 6480c5fa..2227674a 100644
--- a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -24,6 +24,7 @@ import static org.junit.Assert.*;
/**
* @author Arjen Poutsma
+ * @author Juergen Hoeller
*/
public class UriUtilsTests {
@@ -104,4 +105,22 @@ public class UriUtilsTests {
UriUtils.decode("foo%2", ENC);
}
+ @Test
+ public void extractFileExtension() {
+ assertEquals("html", UriUtils.extractFileExtension("index.html"));
+ assertEquals("html", UriUtils.extractFileExtension("/index.html"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html#/a"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html#/path/a"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html#/path/a.do"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=a"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a.do"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a#/path/a"));
+ assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a.do#/path/a.do"));
+ assertEquals("html", UriUtils.extractFileExtension("/products;q=11/view.html?param=/path/a.do"));
+ assertEquals("html", UriUtils.extractFileExtension("/products;q=11/view.html;r=22?param=/path/a.do"));
+ assertEquals("html", UriUtils.extractFileExtension("/products;q=11/view.html;r=22;s=33?param=/path/a.do"));
+ }
+
}
diff --git a/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt b/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt
new file mode 100644
index 00000000..84bbb9dd
--- /dev/null
+++ b/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt
@@ -0,0 +1 @@
+Spring Framework test resource content. \ No newline at end of file
diff --git a/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd b/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd
index 31aa2524..86e8cbab 100644
--- a/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd
+++ b/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd
@@ -1,11 +1,9 @@
-<!-- File containing all charcter entity references definied
+<!-- File containing all character entity references defined
by the HTML 4.0 standard. -->
-<!-- Valuable informations and a complete description of the
+<!-- Valuable information and a complete description of the
HTML 4.0 character set can be found at
- http://www.w3.org/TR/html4/charset.html.
- -->
-
-
+ http://www.w3.org/TR/html4/charset.html. -->
+
<!-- Portions © International Organization for Standardization 1986
Permission to copy in any form is granted for use with
conforming SGML systems and applications as defined in