summaryrefslogtreecommitdiff
path: root/spring-web/src/main/java/org
diff options
context:
space:
mode:
authorEmmanuel Bourg <ebourg@apache.org>2016-08-02 11:13:32 +0200
committerEmmanuel Bourg <ebourg@apache.org>2016-08-02 11:13:32 +0200
commitf69f2a4b8ea697b3a631c0dc7a470e3c9793fee3 (patch)
treedb2f25b29aa3e59c463ab41d3f2856f6265bb1a5 /spring-web/src/main/java/org
parent5575b60c30c5a0c308c4ba3a2db93956d8c1746c (diff)
Imported Upstream version 4.2.6
Diffstat (limited to 'spring-web/src/main/java/org')
-rw-r--r--spring-web/src/main/java/org/springframework/http/CacheControl.java255
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpEntity.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpHeaders.java270
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpMethod.java36
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpRange.java285
-rw-r--r--spring-web/src/main/java/org/springframework/http/HttpRequest.java11
-rw-r--r--spring-web/src/main/java/org/springframework/http/MediaType.java24
-rw-r--r--spring-web/src/main/java/org/springframework/http/RequestEntity.java3
-rw-r--r--spring-web/src/main/java/org/springframework/http/ResponseEntity.java30
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java4
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java4
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java8
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java31
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java4
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java14
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java87
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java15
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java14
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java15
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java69
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java6
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java144
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java116
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java86
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java8
-rw-r--r--spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java12
-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/HttpAccessor.java3
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java124
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java65
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java41
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java68
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java52
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java16
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java2
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java75
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/GsonBuilderUtils.java6
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java39
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java170
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java61
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java24
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonInputMessage.java70
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java27
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java126
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java22
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java27
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java14
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java10
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java9
-rw-r--r--spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java10
-rw-r--r--spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java19
-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.java80
-rw-r--r--spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean.java7
-rw-r--r--spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java7
-rw-r--r--spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java16
-rw-r--r--spring-web/src/main/java/org/springframework/web/SpringServletContainerInitializer.java18
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java48
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java107
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java158
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java10
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java23
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java29
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/MediaTypeFileExtensionResolver.java8
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java42
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java73
-rw-r--r--spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java33
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java71
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java72
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java18
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java39
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java116
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java31
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java36
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java47
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java38
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java26
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java45
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java42
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java17
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/bind/support/SpringWebConstraintValidatorFactory.java69
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java30
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/RestOperations.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/client/RestTemplate.java37
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/AbstractContextLoaderInitializer.java19
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/ContextLoader.java83
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/ContextLoader.properties5
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java1
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java12
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/Log4jNestedDiagnosticContextInterceptor.java3
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java164
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java52
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java5
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java12
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java1
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java43
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/support/ServletContextPropertyPlaceholderConfigurer.java162
-rw-r--r--spring-web/src/main/java/org/springframework/web/context/support/WebApplicationContextUtils.java40
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java393
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationSource.java35
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java50
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java48
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java199
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java134
-rw-r--r--spring-web/src/main/java/org/springframework/web/cors/package-info.java5
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java7
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java36
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java95
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java14
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/Log4jNestedDiagnosticContextFilter.java3
-rw-r--r--spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java85
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java5
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java33
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java38
-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.java17
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java11
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentConversionNotSupportedException.java61
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentTypeMismatchException.java61
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java126
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java13
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java8
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMapMethodArgumentResolver.java8
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java23
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java6
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/annotation/SessionStatusMethodArgumentResolver.java2
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/AsyncHandlerMethodReturnValueHandler.java50
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/CompositeUriComponentsContributor.java16
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java43
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerComposite.java51
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java8
-rw-r--r--spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java8
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java5
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java14
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java4
-rw-r--r--spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java19
-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/multipart/support/StandardMultipartHttpServletRequest.java49
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java6
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java68
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java15
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java130
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java155
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/Log4jConfigListener.java3
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/Log4jWebConfigurer.java10
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java9
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriComponents.java34
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java179
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriTemplate.java130
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java46
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UriUtils.java190
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java41
-rw-r--r--spring-web/src/main/java/org/springframework/web/util/WebUtils.java28
160 files changed, 5715 insertions, 1813 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
new file mode 100644
index 00000000..a291e7ae
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/CacheControl.java
@@ -0,0 +1,255 @@
+/*
+ * 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;
+
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * A builder for creating "Cache-Control" HTTP response headers.
+ *
+ * <p>Adding Cache-Control directives to HTTP responses can significantly improve the client
+ * experience when interacting with a web application. This builder creates opinionated
+ * "Cache-Control" headers with response directives only, with several use cases in mind.
+ *
+ * <ul>
+ * <li>Caching HTTP responses with {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS)}
+ * will result in {@code Cache-Control: "max-age=3600"}</li>
+ * <li>Preventing cache with {@code CacheControl cc = CacheControl.noStore()}
+ * will result in {@code Cache-Control: "no-store"}</li>
+ * <li>Advanced cases like {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()}
+ * will result in {@code Cache-Control: "max-age=3600, no-transform, public"}</li>
+ * </ul>
+ *
+ * <p>Note that to be efficient, Cache-Control headers should be written along HTTP validators
+ * such as "Last-Modified" or "ETag" headers.
+ *
+ * @author Brian Clozel
+ * @author Juergen Hoeller
+ * @since 4.2
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2">rfc7234 section 5.2.2</a>
+ * @see <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">
+ * HTTP caching - Google developers reference</a>
+ * @see <a href="https://www.mnot.net/cache_docs/">Mark Nottingham's cache documentation</a>
+ */
+public class CacheControl {
+
+ private long maxAge = -1;
+
+ private boolean noCache = false;
+
+ private boolean noStore = false;
+
+ private boolean mustRevalidate = false;
+
+ private boolean noTransform = false;
+
+ private boolean cachePublic = false;
+
+ private boolean cachePrivate = false;
+
+ private boolean proxyRevalidate = false;
+
+ private long sMaxAge = -1;
+
+
+ /**
+ * Create an empty CacheControl instance.
+ * @see #empty()
+ */
+ protected CacheControl() {
+ }
+
+
+ /**
+ * Return an empty directive.
+ * <p>This is well suited for using other optional directives without "max-age", "no-cache" or "no-store".
+ * @return {@code this}, to facilitate method chaining
+ */
+ public static CacheControl empty() {
+ return new CacheControl();
+ }
+
+ /**
+ * Add a "max-age=" directive.
+ * <p>This directive is well suited for publicly caching resources, knowing that they won't change within
+ * the configured amount of time. Additional directives can be also used, in case resources shouldn't be
+ * cached ({@link #cachePrivate()}) or transformed ({@link #noTransform()}) by shared caches.
+ * <p>In order to prevent caches to reuse the cached response even when it has become stale
+ * (i.e. the "max-age" delay is passed), the "must-revalidate" directive should be set ({@link #mustRevalidate()}
+ * @param maxAge the maximum time the response should be cached
+ * @param unit the time unit of the {@code maxAge} argument
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
+ */
+ public static CacheControl maxAge(long maxAge, TimeUnit unit) {
+ CacheControl cc = new CacheControl();
+ cc.maxAge = unit.toSeconds(maxAge);
+ return cc;
+ }
+
+ /**
+ * Add a "no-cache" directive.
+ * <p>This directive is well suited for telling caches that the response can be reused only if the client
+ * revalidates it with the server. This directive won't disable cache altogether and may result with
+ * 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.
+ * @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>
+ */
+ public static CacheControl noCache() {
+ CacheControl cc = new CacheControl();
+ cc.noCache = true;
+ return cc;
+ }
+
+ /**
+ * Add a "no-store" directive.
+ * <p>This directive is well suited for preventing caches (browsers and proxies) to cache the content of responses.
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.3">rfc7234 section 5.2.2.3</a>
+ */
+ public static CacheControl noStore() {
+ CacheControl cc = new CacheControl();
+ cc.noStore = true;
+ return cc;
+ }
+
+
+ /**
+ * Add a "must-revalidate" directive.
+ * <p>This directive indicates that once it has become stale, a cache MUST NOT use the response
+ * to satisfy subsequent requests without successful validation on the origin server.
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1">rfc7234 section 5.2.2.1</a>
+ */
+ public CacheControl mustRevalidate() {
+ this.mustRevalidate = true;
+ return this;
+ }
+
+ /**
+ * Add a "no-transform" directive.
+ * <p>This directive indicates that intermediaries (caches and others) should not transform the response content.
+ * This can be useful to force caches and CDNs not to automatically gzip or optimize the response content.
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.4">rfc7234 section 5.2.2.4</a>
+ */
+ public CacheControl noTransform() {
+ this.noTransform = true;
+ return this;
+ }
+
+ /**
+ * Add a "public" directive.
+ * <p>This directive indicates that any cache MAY store the response, even if the response
+ * would normally be non-cacheable or cacheable only within a private cache.
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5">rfc7234 section 5.2.2.5</a>
+ */
+ public CacheControl cachePublic() {
+ this.cachePublic = true;
+ return this;
+ }
+
+ /**
+ * Add a "private" directive.
+ * <p>This directive indicates that the response message is intended for a single user
+ * and MUST NOT be stored by a shared cache.
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6">rfc7234 section 5.2.2.6</a>
+ */
+ public CacheControl cachePrivate() {
+ this.cachePrivate = true;
+ return this;
+ }
+
+ /**
+ * Add a "proxy-revalidate" directive.
+ * <p>This directive has the same meaning as the "must-revalidate" directive,
+ * except that it does not apply to private caches (i.e. browsers, HTTP clients).
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.7">rfc7234 section 5.2.2.7</a>
+ */
+ public CacheControl proxyRevalidate() {
+ this.proxyRevalidate = true;
+ return this;
+ }
+
+ /**
+ * Add an "s-maxage" directive.
+ * <p>This directive indicates that, in shared caches, the maximum age specified by this directive
+ * overrides the maximum age specified by other directives.
+ * @param sMaxAge the maximum time the response should be cached
+ * @param unit the time unit of the {@code sMaxAge} argument
+ * @return {@code this}, to facilitate method chaining
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
+ */
+ public CacheControl sMaxAge(long sMaxAge, TimeUnit unit) {
+ this.sMaxAge = unit.toSeconds(sMaxAge);
+ return this;
+ }
+
+
+ /**
+ * Return the "Cache-Control" header value.
+ * @return {@code null} if no directive was added, or the header value otherwise
+ */
+ public String getHeaderValue() {
+ StringBuilder ccValue = new StringBuilder();
+ if (this.maxAge != -1) {
+ appendDirective(ccValue, "max-age=" + Long.toString(this.maxAge));
+ }
+ if (this.noCache) {
+ appendDirective(ccValue, "no-cache");
+ }
+ if (this.noStore) {
+ appendDirective(ccValue, "no-store");
+ }
+ if (this.mustRevalidate) {
+ appendDirective(ccValue, "must-revalidate");
+ }
+ if (this.noTransform) {
+ appendDirective(ccValue, "no-transform");
+ }
+ if (this.cachePublic) {
+ appendDirective(ccValue, "public");
+ }
+ if (this.cachePrivate) {
+ appendDirective(ccValue, "private");
+ }
+ if (this.proxyRevalidate) {
+ appendDirective(ccValue, "proxy-revalidate");
+ }
+ if (this.sMaxAge != -1) {
+ appendDirective(ccValue, "s-maxage=" + Long.toString(this.sMaxAge));
+ }
+ String ccHeaderValue = ccValue.toString();
+ return (StringUtils.hasText(ccHeaderValue) ? ccHeaderValue : null);
+ }
+
+ private void appendDirective(StringBuilder builder, String value) {
+ if (builder.length() > 0) {
+ builder.append(", ");
+ }
+ builder.append(value);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpEntity.java b/spring-web/src/main/java/org/springframework/http/HttpEntity.java
index d9697697..b866c0bf 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpEntity.java
@@ -131,7 +131,7 @@ public class HttpEntity<T> {
if (this == other) {
return true;
}
- if (other == null || !other.getClass().equals(getClass())) {
+ if (other == null || other.getClass() != getClass()) {
return false;
}
HttpEntity<?> otherEntity = (HttpEntity<?>) other;
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 6eb106d4..1170bb06 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.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.
@@ -41,7 +41,7 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
- * Represents HTTP request and response headers, mapping string header names to list of string values.
+ * Represents HTTP request and response headers, mapping string header names to a list of string values.
*
* <p>In addition to the normal methods defined by {@link Map}, this class offers the following
* convenience methods:
@@ -51,7 +51,7 @@ import org.springframework.util.StringUtils;
* <li>{@link #set(String, String)} sets the header value to a single string value</li>
* </ul>
*
- * <p>Inspired by {@link com.sun.net.httpserver.Headers}.
+ * <p>Inspired by {@code com.sun.net.httpserver.Headers}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
@@ -87,6 +87,46 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
*/
public static final String ACCEPT_RANGES = "Accept-Ranges";
/**
+ * The CORS {@code Access-Control-Allow-Credentials} response header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
+ /**
+ * The CORS {@code Access-Control-Allow-Headers} response header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
+ /**
+ * The CORS {@code Access-Control-Allow-Methods} response header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
+ /**
+ * The CORS {@code Access-Control-Allow-Origin} response header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
+ /**
+ * The CORS {@code Access-Control-Expose-Headers} response header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
+ /**
+ * The CORS {@code Access-Control-Max-Age} response header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
+ /**
+ * The CORS {@code Access-Control-Request-Headers} request header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
+ /**
+ * The CORS {@code Access-Control-Request-Method} request header field name.
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ */
+ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
+ /**
* The HTTP {@code Age} header field name.
* @see <a href="http://tools.ietf.org/html/rfc7234#section-5.1">Section 5.1 of RFC 7234</a>
*/
@@ -322,10 +362,14 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
*/
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+ /**
+ * Date formats as specified in the HTTP RFC
+ * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
+ */
private static final String[] DATE_FORMATS = new String[] {
- "EEE, dd MMM yyyy HH:mm:ss zzz",
- "EEE, dd-MMM-yy HH:mm:ss zzz",
- "EEE MMM dd HH:mm:ss yyyy"
+ "EEE, dd MMM yyyy HH:mm:ss zzz",
+ "EEE, dd-MMM-yy HH:mm:ss zzz",
+ "EEE MMM dd HH:mm:ss yyyy"
};
private static TimeZone GMT = TimeZone.getTimeZone("GMT");
@@ -391,6 +435,131 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
+ * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
+ */
+ public void setAccessControlAllowCredentials(boolean allowCredentials) {
+ set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials));
+ }
+
+ /**
+ * Returns the value of the {@code Access-Control-Allow-Credentials} response header.
+ */
+ public boolean getAccessControlAllowCredentials() {
+ return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS));
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Allow-Headers} response header.
+ */
+ public void setAccessControlAllowHeaders(List<String> allowedHeaders) {
+ set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders));
+ }
+
+ /**
+ * Returns the value of the {@code Access-Control-Allow-Headers} response header.
+ */
+ public List<String> getAccessControlAllowHeaders() {
+ return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS);
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Allow-Methods} response header.
+ */
+ public void setAccessControlAllowMethods(List<HttpMethod> allowedMethods) {
+ set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods));
+ }
+
+ /**
+ * Return the value of the {@code Access-Control-Allow-Methods} response header.
+ */
+ public List<HttpMethod> getAccessControlAllowMethods() {
+ List<HttpMethod> result = new ArrayList<HttpMethod>();
+ String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS);
+ if (value != null) {
+ String[] tokens = value.split(",\\s*");
+ for (String token : tokens) {
+ HttpMethod resolved = HttpMethod.resolve(token);
+ if (resolved != null) {
+ result.add(resolved);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Allow-Origin} response header.
+ */
+ public void setAccessControlAllowOrigin(String allowedOrigin) {
+ set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin);
+ }
+
+ /**
+ * Return the value of the {@code Access-Control-Allow-Origin} response header.
+ */
+ public String getAccessControlAllowOrigin() {
+ return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN);
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Expose-Headers} response header.
+ */
+ public void setAccessControlExposeHeaders(List<String> exposedHeaders) {
+ set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders));
+ }
+
+ /**
+ * Returns the value of the {@code Access-Control-Expose-Headers} response header.
+ */
+ public List<String> getAccessControlExposeHeaders() {
+ return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS);
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Max-Age} response header.
+ */
+ public void setAccessControlMaxAge(long maxAge) {
+ set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge));
+ }
+
+ /**
+ * Returns the value of the {@code Access-Control-Max-Age} response header.
+ * <p>Returns -1 when the max age is unknown.
+ */
+ public long getAccessControlMaxAge() {
+ String value = getFirst(ACCESS_CONTROL_MAX_AGE);
+ return (value != null ? Long.parseLong(value) : -1);
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Request-Headers} request header.
+ */
+ public void setAccessControlRequestHeaders(List<String> requestHeaders) {
+ set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders));
+ }
+
+ /**
+ * Returns the value of the {@code Access-Control-Request-Headers} request header.
+ */
+ public List<String> getAccessControlRequestHeaders() {
+ return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS);
+ }
+
+ /**
+ * Set the (new) value of the {@code Access-Control-Request-Method} request header.
+ */
+ public void setAccessControlRequestMethod(HttpMethod requestedMethod) {
+ set(ACCESS_CONTROL_REQUEST_METHOD, requestedMethod.name());
+ }
+
+ /**
+ * Return the value of the {@code Access-Control-Request-Method} request header.
+ */
+ public HttpMethod getAccessControlRequestMethod() {
+ return HttpMethod.resolve(getFirst(ACCESS_CONTROL_REQUEST_METHOD));
+ }
+
+ /**
* Set the list of acceptable {@linkplain Charset charsets},
* as specified by the {@code Accept-Charset} header.
*/
@@ -451,7 +620,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
List<HttpMethod> result = new LinkedList<HttpMethod>();
String[] tokens = value.split(",\\s*");
for (String token : tokens) {
- result.add(HttpMethod.valueOf(token));
+ HttpMethod resolved = HttpMethod.resolve(token);
+ if (resolved != null) {
+ result.add(resolved);
+ }
}
return EnumSet.copyOf(result);
}
@@ -607,12 +779,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* January 1, 1970 GMT. Returns -1 when the date is unknown.
*/
public long getExpires() {
- try {
- return getFirstDate(EXPIRES);
- }
- catch (IllegalArgumentException ex) {
- return -1;
- }
+ return getFirstDate(EXPIRES, false);
}
/**
@@ -625,23 +792,12 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
- * Return the value of the {@code IfModifiedSince} header.
- * <p>The date is returned as the number of milliseconds since
- * January 1, 1970 GMT. Returns -1 when the date is unknown.
- * @deprecated use {@link #getIfModifiedSince()}
- */
- @Deprecated
- public long getIfNotModifiedSince() {
- return getIfModifiedSince();
- }
-
- /**
* Return the value of the {@code If-Modified-Since} header.
* <p>The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
*/
public long getIfModifiedSince() {
- return getFirstDate(IF_MODIFIED_SINCE);
+ return getFirstDate(IF_MODIFIED_SINCE, false);
}
/**
@@ -706,7 +862,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* January 1, 1970 GMT. Returns -1 when the date is unknown.
*/
public long getLastModified() {
- return getFirstDate(LAST_MODIFIED);
+ return getFirstDate(LAST_MODIFIED, false);
}
/**
@@ -756,6 +912,23 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
}
/**
+ * Sets the (new) value of the {@code Range} header.
+ */
+ public void setRange(List<HttpRange> ranges) {
+ String value = HttpRange.toString(ranges);
+ set(RANGE, value);
+ }
+
+ /**
+ * Return the value of the {@code Range} header.
+ * <p>Returns an empty list when the range is unknown.
+ */
+ public List<HttpRange> getRange() {
+ String value = getFirst(RANGE);
+ return HttpRange.parseRanges(value);
+ }
+
+ /**
* Set the (new) value of the {@code Upgrade} header.
*/
public void setUpgrade(String upgrade) {
@@ -773,24 +946,49 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* 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
*/
public long getFirstDate(String headerName) {
+ return getFirstDate(headerName, true);
+ }
+
+ /**
+ * Parse the first header value for the given header name as a date,
+ * return -1 if there is no value or also in case of an invalid value
+ * (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException}
+ * if the value cannot be parsed as a date.
+ * @param headerName the header name
+ * @param rejectInvalid whether to reject invalid values with an
+ * {@link IllegalArgumentException} ({@code true}) or rather return -1
+ * in that case ({@code false})
+ * @return the parsed date header, or -1 if none (or invalid)
+ */
+ private long getFirstDate(String headerName, boolean rejectInvalid) {
String headerValue = getFirst(headerName);
if (headerValue == null) {
+ // No header value sent at all
return -1;
}
- for (String dateFormat : DATE_FORMATS) {
- SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
- simpleDateFormat.setTimeZone(GMT);
- try {
- return simpleDateFormat.parse(headerValue).getTime();
- }
- catch (ParseException ex) {
- // ignore
+ if (headerValue.length() >= 3) {
+ // Short "0" or "-1" like values are never valid HTTP date headers...
+ // Let's only bother with SimpleDateFormat parsing for long enough values.
+ for (String dateFormat : DATE_FORMATS) {
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
+ simpleDateFormat.setTimeZone(GMT);
+ try {
+ return simpleDateFormat.parse(headerValue).getTime();
+ }
+ catch (ParseException ex) {
+ // ignore
+ }
}
}
- throw new IllegalArgumentException("Cannot parse date value \"" + headerValue +
- "\" for \"" + headerName + "\" header");
+ if (rejectInvalid) {
+ throw new IllegalArgumentException("Cannot parse date value \"" + headerValue +
+ "\" for \"" + headerName + "\" header");
+ }
+ return -1;
}
/**
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 2f1bcd2b..87173fd3 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java
@@ -16,16 +16,52 @@
package org.springframework.http;
+import java.util.HashMap;
+import java.util.Map;
+
/**
* Java 5 enumeration of HTTP request methods. Intended for use
* with {@link org.springframework.http.client.ClientHttpRequest}
* and {@link org.springframework.web.client.RestTemplate}.
*
* @author Arjen Poutsma
+ * @author Juergen Hoeller
* @since 3.0
*/
public enum HttpMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
+
+ private static final Map<String, HttpMethod> mappings = new HashMap<String, HttpMethod>(8);
+
+ static {
+ for (HttpMethod httpMethod : values()) {
+ mappings.put(httpMethod.name(), httpMethod);
+ }
+ }
+
+
+ /**
+ * Resolve the given method value to an {@code HttpMethod}.
+ * @param method the method value as a String
+ * @return the corresponding {@code HttpMethod}, or {@code null} if not found
+ * @since 4.2.4
+ */
+ public static HttpMethod resolve(String method) {
+ return (method != null ? mappings.get(method) : null);
+ }
+
+
+ /**
+ * Determine whether this {@code HttpMethod} matches the given
+ * method value.
+ * @param method the method value as a String
+ * @return {@code true} if it matches, {@code false} otherwise
+ * @since 4.2.4
+ */
+ public boolean matches(String method) {
+ return name().equals(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
new file mode 100644
index 00000000..29f2e675
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java
@@ -0,0 +1,285 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Represents an HTTP (byte) range for use with the HTTP {@code "Range"} header.
+ *
+ * @author Arjen Poutsma
+ * @author Juergen Hoeller
+ * @since 4.2
+ * @see <a href="http://tools.ietf.org/html/rfc7233">HTTP/1.1: Range Requests</a>
+ * @see HttpHeaders#setRange(List)
+ * @see HttpHeaders#getRange()
+ */
+public abstract class HttpRange {
+
+ private static final String BYTE_RANGE_PREFIX = "bytes=";
+
+
+ /**
+ * 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
+ */
+ public abstract long getRangeStart(long length);
+
+ /**
+ * Return the end of the range (inclusive) given the total length of a representation.
+ * @param length the length of the representation
+ * @return the end of the range for the representation
+ */
+ public abstract long getRangeEnd(long length);
+
+
+ /**
+ * Create an {@code HttpRange} from the given position to the end.
+ * @param firstBytePos the first byte position
+ * @return a byte range that ranges from {@code firstPos} till the end
+ * @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
+ */
+ public static HttpRange createByteRange(long firstBytePos) {
+ return new ByteRange(firstBytePos, null);
+ }
+
+ /**
+ * Create a {@code HttpRange} from the given fist to last position.
+ * @param firstBytePos the first byte position
+ * @param lastBytePos the last byte position
+ * @return a byte range that ranges from {@code firstPos} till {@code lastPos}
+ * @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
+ */
+ public static HttpRange createByteRange(long firstBytePos, long lastBytePos) {
+ return new ByteRange(firstBytePos, lastBytePos);
+ }
+
+ /**
+ * Create an {@code HttpRange} that ranges over the last given number of bytes.
+ * @param suffixLength the number of bytes for the range
+ * @return a byte range that ranges over the last {@code suffixLength} number of bytes
+ * @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
+ */
+ public static HttpRange createSuffixRange(long suffixLength) {
+ return new SuffixByteRange(suffixLength);
+ }
+
+ /**
+ * Parse the given, comma-separated string into a list of {@code HttpRange} objects.
+ * <p>This method can be used to parse an {@code Range} header.
+ * @param ranges the string to parse
+ * @return the list of ranges
+ * @throws IllegalArgumentException if the string cannot be parsed
+ */
+ public static List<HttpRange> parseRanges(String ranges) {
+ if (!StringUtils.hasLength(ranges)) {
+ return Collections.emptyList();
+ }
+ if (!ranges.startsWith(BYTE_RANGE_PREFIX)) {
+ throw new IllegalArgumentException("Range '" + ranges + "' does not start with 'bytes='");
+ }
+ ranges = ranges.substring(BYTE_RANGE_PREFIX.length());
+
+ String[] tokens = ranges.split(",\\s*");
+ List<HttpRange> result = new ArrayList<HttpRange>(tokens.length);
+ for (String token : tokens) {
+ result.add(parseRange(token));
+ }
+ return result;
+ }
+
+ private static HttpRange parseRange(String range) {
+ Assert.hasLength(range, "Range String must not be empty");
+ int dashIdx = range.indexOf('-');
+ if (dashIdx > 0) {
+ long firstPos = Long.parseLong(range.substring(0, dashIdx));
+ if (dashIdx < range.length() - 1) {
+ Long lastPos = Long.parseLong(range.substring(dashIdx + 1, range.length()));
+ return new ByteRange(firstPos, lastPos);
+ }
+ else {
+ return new ByteRange(firstPos, null);
+ }
+ }
+ else if (dashIdx == 0) {
+ long suffixLength = Long.parseLong(range.substring(1));
+ return new SuffixByteRange(suffixLength);
+ }
+ else {
+ throw new IllegalArgumentException("Range '" + range + "' does not contain \"-\"");
+ }
+ }
+
+ /**
+ * 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
+ * @return the string representation
+ */
+ public static String toString(Collection<HttpRange> ranges) {
+ Assert.notEmpty(ranges, "Ranges Collection must not be empty");
+ StringBuilder builder = new StringBuilder(BYTE_RANGE_PREFIX);
+ for (Iterator<HttpRange> iterator = ranges.iterator(); iterator.hasNext(); ) {
+ HttpRange range = iterator.next();
+ builder.append(range);
+ if (iterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ return builder.toString();
+ }
+
+
+ /**
+ * Represents an HTTP/1.1 byte range, with a first and optional last position.
+ * @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
+ * @see HttpRange#createByteRange(long)
+ * @see HttpRange#createByteRange(long, long)
+ */
+ private static class ByteRange extends HttpRange {
+
+ private final long firstPos;
+
+ private final Long lastPos;
+
+ public ByteRange(long firstPos, Long lastPos) {
+ assertPositions(firstPos, lastPos);
+ this.firstPos = firstPos;
+ this.lastPos = lastPos;
+ }
+
+ private void assertPositions(long firstBytePos, Long lastBytePos) {
+ if (firstBytePos < 0) {
+ throw new IllegalArgumentException("Invalid first byte position: " + firstBytePos);
+ }
+ if (lastBytePos != null && lastBytePos < firstBytePos) {
+ throw new IllegalArgumentException("firstBytePosition=" + firstBytePos +
+ " should be less then or equal to lastBytePosition=" + lastBytePos);
+ }
+ }
+
+ @Override
+ public long getRangeStart(long length) {
+ return this.firstPos;
+ }
+
+ @Override
+ public long getRangeEnd(long length) {
+ if (this.lastPos != null && this.lastPos < length) {
+ return this.lastPos;
+ }
+ else {
+ return length - 1;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ByteRange)) {
+ return false;
+ }
+ ByteRange otherRange = (ByteRange) other;
+ return (this.firstPos == otherRange.firstPos &&
+ ObjectUtils.nullSafeEquals(this.lastPos, otherRange.lastPos));
+ }
+
+ @Override
+ public int hashCode() {
+ return (ObjectUtils.nullSafeHashCode(this.firstPos) * 31 +
+ ObjectUtils.nullSafeHashCode(this.lastPos));
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(this.firstPos);
+ builder.append('-');
+ if (this.lastPos != null) {
+ builder.append(this.lastPos);
+ }
+ return builder.toString();
+ }
+ }
+
+
+ /**
+ * Represents an HTTP/1.1 suffix byte range, with a number of suffix bytes.
+ * @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
+ * @see HttpRange#createSuffixRange(long)
+ */
+ private static class SuffixByteRange extends HttpRange {
+
+ private final long suffixLength;
+
+ public SuffixByteRange(long suffixLength) {
+ if (suffixLength < 0) {
+ throw new IllegalArgumentException("Invalid suffix length: " + suffixLength);
+ }
+ this.suffixLength = suffixLength;
+ }
+
+ @Override
+ public long getRangeStart(long length) {
+ if (this.suffixLength < length) {
+ return length - this.suffixLength;
+ }
+ else {
+ return 0;
+ }
+ }
+
+ @Override
+ public long getRangeEnd(long length) {
+ return length - 1;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SuffixByteRange)) {
+ return false;
+ }
+ SuffixByteRange otherRange = (SuffixByteRange) other;
+ return (this.suffixLength == otherRange.suffixLength);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.hashCode(this.suffixLength);
+ }
+
+ @Override
+ public String toString() {
+ return "-" + this.suffixLength;
+ }
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/HttpRequest.java b/spring-web/src/main/java/org/springframework/http/HttpRequest.java
index b84df266..2f670a37 100644
--- a/spring-web/src/main/java/org/springframework/http/HttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/HttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2011 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.
@@ -19,8 +19,8 @@ package org.springframework.http;
import java.net.URI;
/**
- * Represents an HTTP request message, consisting of {@linkplain #getMethod() method}
- * and {@linkplain #getURI() uri}.
+ * Represents an HTTP request message, consisting of
+ * {@linkplain #getMethod() method} and {@linkplain #getURI() uri}.
*
* @author Arjen Poutsma
* @since 3.1
@@ -29,13 +29,14 @@ public interface HttpRequest extends HttpMessage {
/**
* Return the HTTP method of the request.
- * @return the HTTP method as an HttpMethod enum value
+ * @return the HTTP method as an HttpMethod enum value, or {@code null}
+ * if not resolvable (e.g. in case of a non-standard HTTP method)
*/
HttpMethod getMethod();
/**
* Return the URI of the request.
- * @return the URI of the request
+ * @return the URI of the request (never {@code null})
*/
URI getURI();
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 9ae41f23..8b8e0485 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-2014 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.
@@ -40,6 +40,7 @@ import org.springframework.util.comparator.CompoundComparator;
* @author Arjen Poutsma
* @author Juergen Hoeller
* @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>
@@ -80,15 +81,27 @@ public class MediaType extends MimeType implements Serializable {
/**
* Public constant media type for {@code application/json}.
- * */
+ * @see #APPLICATION_JSON_UTF8
+ */
public final static MediaType APPLICATION_JSON;
/**
* A String equivalent of {@link MediaType#APPLICATION_JSON}.
+ * @see #APPLICATION_JSON_UTF8_VALUE
*/
public final static String APPLICATION_JSON_VALUE = "application/json";
/**
+ * Public constant media type for {@code application/json;charset=UTF-8}.
+ */
+ public final static MediaType APPLICATION_JSON_UTF8;
+
+ /**
+ * A String equivalent of {@link MediaType#APPLICATION_JSON_UTF8}.
+ */
+ public final static String APPLICATION_JSON_UTF8_VALUE = APPLICATION_JSON_VALUE + ";charset=UTF-8";
+
+ /**
* Public constant media type for {@code application/octet-stream}.
* */
public final static MediaType APPLICATION_OCTET_STREAM;
@@ -197,6 +210,7 @@ public class MediaType extends MimeType implements Serializable {
APPLICATION_ATOM_XML = valueOf(APPLICATION_ATOM_XML_VALUE);
APPLICATION_FORM_URLENCODED = valueOf(APPLICATION_FORM_URLENCODED_VALUE);
APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE);
+ APPLICATION_JSON_UTF8 = valueOf(APPLICATION_JSON_UTF8_VALUE);
APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE);
APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE);
APPLICATION_XML = valueOf(APPLICATION_XML_VALUE);
@@ -276,6 +290,7 @@ public class MediaType extends MimeType implements Serializable {
}
+ @Override
protected void checkParameters(String attribute, String value) {
super.checkParameters(attribute, value);
if (PARAM_QUALITY_FACTOR.equals(attribute)) {
@@ -400,9 +415,8 @@ public class MediaType extends MimeType implements Serializable {
/**
* 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 string to parse
- * @return the list of media types
- * @throws IllegalArgumentException if the String cannot be parsed
+ * @param mediaTypes the media types to create a string representation for
+ * @return the string representation
*/
public static String toString(Collection<MediaType> mediaTypes) {
return MimeTypeUtils.toString(mediaTypes);
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 e7e4b5e7..7e9ab88b 100644
--- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java
@@ -20,7 +20,6 @@ import java.net.URI;
import java.nio.charset.Charset;
import java.util.Arrays;
-import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
@@ -104,8 +103,6 @@ public class RequestEntity<T> extends HttpEntity<T> {
*/
public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url) {
super(body, headers);
- Assert.notNull(method, "'method' is required");
- Assert.notNull(url, "'url' is required");
this.method = method;
this.url = url;
}
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 db670723..9eba72c0 100644
--- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java
+++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java
@@ -59,6 +59,7 @@ import org.springframework.util.ObjectUtils;
* </pre>
*
* @author Arjen Poutsma
+ * @author Brian Clozel
* @since 3.0.2
* @see #getStatusCode()
*/
@@ -320,6 +321,18 @@ public class ResponseEntity<T> extends HttpEntity<T> {
B location(URI location);
/**
+ * Set the caching directives for the resource, as specified by the HTTP 1.1
+ * {@code Cache-Control} header.
+ * <p>A {@code CacheControl} instance can be built like
+ * {@code CacheControl.maxAge(3600).cachePublic().noTransform()}.
+ * @param cacheControl a builder for cache-related HTTP response headers
+ * @return this builder
+ * @since 4.2
+ * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">RFC-7234 Section 5.2</a>
+ */
+ B cacheControl(CacheControl cacheControl);
+
+ /**
* Build the response entity with no body.
* @return the response entity
* @see BodyBuilder#body(Object)
@@ -408,6 +421,14 @@ public class ResponseEntity<T> extends HttpEntity<T> {
@Override
public BodyBuilder eTag(String eTag) {
+ if (eTag != null) {
+ if (!eTag.startsWith("\"") && !eTag.startsWith("W/\"")) {
+ eTag = "\"" + eTag;
+ }
+ if (!eTag.endsWith("\"")) {
+ eTag = eTag + "\"";
+ }
+ }
this.headers.setETag(eTag);
return this;
}
@@ -425,6 +446,15 @@ public class ResponseEntity<T> extends HttpEntity<T> {
}
@Override
+ public BodyBuilder cacheControl(CacheControl cacheControl) {
+ String ccValue = cacheControl.getHeaderValue();
+ if (ccValue != null) {
+ this.headers.setCacheControl(cacheControl.getHeaderValue());
+ }
+ return this;
+ }
+
+ @Override
public ResponseEntity<Void> build() {
return new ResponseEntity<Void>(null, this.headers, this.status);
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java
index cf83983d..2bcbcdb2 100644
--- a/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.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.
@@ -43,7 +43,7 @@ abstract class AbstractBufferingAsyncClientHttpRequest extends AbstractAsyncClie
@Override
protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers) throws IOException {
byte[] bytes = this.bufferedOutput.toByteArray();
- if (headers.getContentLength() == -1) {
+ if (headers.getContentLength() < 0) {
headers.setContentLength(bytes.length);
}
ListenableFuture<ClientHttpResponse> result = executeInternal(headers, bytes);
diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java
index 4af73487..d468b587 100644
--- a/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.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,7 +42,7 @@ abstract class AbstractBufferingClientHttpRequest extends AbstractClientHttpRequ
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
byte[] bytes = this.bufferedOutput.toByteArray();
- if (headers.getContentLength() == -1) {
+ if (headers.getContentLength() < 0) {
headers.setContentLength(bytes.length);
}
ClientHttpResponse result = executeInternal(headers, bytes);
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java
index 1043ef79..e062ba8d 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -68,7 +68,7 @@ final class HttpComponentsAsyncClientHttpRequest extends AbstractBufferingAsyncC
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.httpRequest.getMethod());
+ return HttpMethod.resolve(this.httpRequest.getMethod());
}
@Override
@@ -76,6 +76,10 @@ final class HttpComponentsAsyncClientHttpRequest extends AbstractBufferingAsyncC
return this.httpRequest.getURI();
}
+ HttpContext getHttpContext() {
+ return this.httpContext;
+ }
+
@Override
protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] bufferedOutput)
throws IOException {
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java
index 005e5cce..fa57fbd3 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java
@@ -40,6 +40,7 @@ import org.springframework.util.Assert;
* HttpAsyncClient 4.0</a> to create requests.
*
* @author Arjen Poutsma
+ * @author Stephane Nicoll
* @since 4.0
* @see HttpAsyncClient
*/
@@ -64,7 +65,7 @@ public class HttpComponentsAsyncClientHttpRequestFactory extends HttpComponentsC
*/
public HttpComponentsAsyncClientHttpRequestFactory(CloseableHttpAsyncClient httpAsyncClient) {
super();
- Assert.notNull(httpAsyncClient, "'httpAsyncClient' must not be null");
+ Assert.notNull(httpAsyncClient, "HttpAsyncClient must not be null");
this.httpAsyncClient = httpAsyncClient;
}
@@ -78,7 +79,7 @@ public class HttpComponentsAsyncClientHttpRequestFactory extends HttpComponentsC
CloseableHttpClient httpClient, CloseableHttpAsyncClient httpAsyncClient) {
super(httpClient);
- Assert.notNull(httpAsyncClient, "'httpAsyncClient' must not be null");
+ Assert.notNull(httpAsyncClient, "HttpAsyncClient must not be null");
this.httpAsyncClient = httpAsyncClient;
}
@@ -122,18 +123,20 @@ public class HttpComponentsAsyncClientHttpRequestFactory extends HttpComponentsC
if (context == null) {
context = HttpClientContext.create();
}
- // Request configuration not set in the context
- if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
- // Use request configuration given by the user, when available
- RequestConfig config = null;
- if (httpRequest instanceof Configurable) {
- config = ((Configurable) httpRequest).getConfig();
- }
- if (config == null) {
- config = RequestConfig.DEFAULT;
- }
- context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
- }
+ // Request configuration not set in the context
+ if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
+ // Use request configuration given by the user, when available
+ RequestConfig config = null;
+ if (httpRequest instanceof Configurable) {
+ config = ((Configurable) httpRequest).getConfig();
+ }
+ if (config == null) {
+ config = createRequestConfig(asyncClient);
+ }
+ if (config != null) {
+ context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
+ }
+ }
return new HttpComponentsAsyncClientHttpRequest(asyncClient, httpRequest, context);
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java
index 31479e5c..f25579a4 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java
@@ -16,7 +16,6 @@
package org.springframework.http.client;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -25,6 +24,7 @@ import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.springframework.http.HttpHeaders;
+import org.springframework.util.StreamUtils;
/**
* {@link ClientHttpResponse} implementation that uses
@@ -73,7 +73,7 @@ final class HttpComponentsAsyncClientHttpResponse extends AbstractClientHttpResp
@Override
public InputStream getBody() throws IOException {
HttpEntity entity = this.httpResponse.getEntity();
- return (entity != null ? entity.getContent() : new ByteArrayInputStream(new byte[0]));
+ return (entity != null ? entity.getContent() : StreamUtils.emptyInput());
}
@Override
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java
index 7706b365..0bf8d9fb 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -23,10 +23,10 @@ import java.util.Map;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
-import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
@@ -50,14 +50,14 @@ import org.springframework.util.StringUtils;
*/
final class HttpComponentsClientHttpRequest extends AbstractBufferingClientHttpRequest {
- private final CloseableHttpClient httpClient;
+ private final HttpClient httpClient;
private final HttpUriRequest httpRequest;
private final HttpContext httpContext;
- HttpComponentsClientHttpRequest(CloseableHttpClient httpClient, HttpUriRequest httpRequest, HttpContext httpContext) {
+ HttpComponentsClientHttpRequest(HttpClient httpClient, HttpUriRequest httpRequest, HttpContext httpContext) {
this.httpClient = httpClient;
this.httpRequest = httpRequest;
this.httpContext = httpContext;
@@ -66,7 +66,7 @@ final class HttpComponentsClientHttpRequest extends AbstractBufferingClientHttpR
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.httpRequest.getMethod());
+ return HttpMethod.resolve(this.httpRequest.getMethod());
}
@Override
@@ -88,7 +88,7 @@ final class HttpComponentsClientHttpRequest extends AbstractBufferingClientHttpR
HttpEntity requestEntity = new ByteArrayEntity(bufferedOutput);
entityEnclosingRequest.setEntity(requestEntity);
}
- CloseableHttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
+ HttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
return new HttpComponentsClientHttpResponse(httpResponse);
}
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 a318259b..53f6faf0 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
@@ -16,6 +16,7 @@
package org.springframework.http.client;
+import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
@@ -32,7 +33,6 @@ import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
@@ -53,11 +53,12 @@ import org.springframework.util.Assert;
* @author Oleg Kalnichevski
* @author Arjen Poutsma
* @author Stephane Nicoll
+ * @author Juergen Hoeller
* @since 3.1
*/
public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {
- private CloseableHttpClient httpClient;
+ private HttpClient httpClient;
private RequestConfig requestConfig;
@@ -75,25 +76,20 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
/**
* Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
* with the given {@link HttpClient} instance.
- * <p>As of Spring Framework 4.0, the given client is expected to be of type
- * {@link CloseableHttpClient} (requiring HttpClient 4.3+).
* @param httpClient the HttpClient instance to use for this request factory
*/
public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
- Assert.notNull(httpClient, "'httpClient' must not be null");
- Assert.isInstanceOf(CloseableHttpClient.class, httpClient, "'httpClient' is not of type CloseableHttpClient");
- this.httpClient = (CloseableHttpClient) httpClient;
+ Assert.notNull(httpClient, "HttpClient must not be null");
+ this.httpClient = httpClient;
}
/**
* Set the {@code HttpClient} used for
- * <p>As of Spring Framework 4.0, the given client is expected to be of type
- * {@link CloseableHttpClient} (requiring HttpClient 4.3+).
+ * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}.
*/
public void setHttpClient(HttpClient httpClient) {
- Assert.isInstanceOf(CloseableHttpClient.class, httpClient, "'httpClient' is not of type CloseableHttpClient");
- this.httpClient = (CloseableHttpClient) httpClient;
+ this.httpClient = httpClient;
}
/**
@@ -114,7 +110,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
*/
public void setConnectTimeout(int timeout) {
Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
- this.requestConfig = cloneRequestConfig().setConnectTimeout(timeout).build();
+ this.requestConfig = requestConfigBuilder().setConnectTimeout(timeout).build();
setLegacyConnectionTimeout(getHttpClient(), timeout);
}
@@ -149,7 +145,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
* @see RequestConfig#getConnectionRequestTimeout()
*/
public void setConnectionRequestTimeout(int connectionRequestTimeout) {
- this.requestConfig = cloneRequestConfig().setConnectionRequestTimeout(connectionRequestTimeout).build();
+ this.requestConfig = requestConfigBuilder().setConnectionRequestTimeout(connectionRequestTimeout).build();
}
/**
@@ -162,7 +158,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
*/
public void setReadTimeout(int timeout) {
Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
- this.requestConfig = cloneRequestConfig().setSocketTimeout(timeout).build();
+ this.requestConfig = requestConfigBuilder().setSocketTimeout(timeout).build();
setLegacySocketTimeout(getHttpClient(), timeout);
}
@@ -192,8 +188,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
- CloseableHttpClient client = (CloseableHttpClient) getHttpClient();
+ HttpClient client = getHttpClient();
Assert.state(client != null, "Synchronous execution requires an HttpClient to be set");
+
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
postProcessHttpRequest(httpRequest);
HttpContext context = createHttpContext(httpMethod, uri);
@@ -209,7 +206,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
config = ((Configurable) httpRequest).getConfig();
}
if (config == null) {
- config = this.requestConfig;
+ config = createRequestConfig(client);
}
if (config != null) {
context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
@@ -225,11 +222,63 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
}
- private RequestConfig.Builder cloneRequestConfig() {
+ /**
+ * Return a builder for modifying the factory-level {@link RequestConfig}.
+ * @since 4.2
+ */
+ private RequestConfig.Builder requestConfigBuilder() {
return (this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom());
}
/**
+ * Create a default {@link RequestConfig} to use with the given client.
+ * Can return {@code null} to indicate that no custom request config should
+ * be set and the defaults of the {@link HttpClient} should be used.
+ * <p>The default implementation tries to merge the defaults of the client
+ * with the local customizations of this factory instance, if any.
+ * @param client the {@link HttpClient} (or {@code HttpAsyncClient}) to check
+ * @return the actual RequestConfig to use (may be {@code null})
+ * @since 4.2
+ * @see #mergeRequestConfig(RequestConfig)
+ */
+ protected RequestConfig createRequestConfig(Object client) {
+ if (client instanceof Configurable) {
+ RequestConfig clientRequestConfig = ((Configurable) client).getConfig();
+ return mergeRequestConfig(clientRequestConfig);
+ }
+ return this.requestConfig;
+ }
+
+ /**
+ * Merge the given {@link HttpClient}-level {@link RequestConfig} with
+ * the factory-level {@link RequestConfig}, if necessary.
+ * @param clientConfig the config held by the current
+ * @return the merged request config
+ * (may be {@code null} if the given client config is {@code null})
+ * @since 4.2
+ */
+ protected RequestConfig mergeRequestConfig(RequestConfig clientConfig) {
+ if (this.requestConfig == null) { // nothing to merge
+ return clientConfig;
+ }
+
+ RequestConfig.Builder builder = RequestConfig.copy(clientConfig);
+ int connectTimeout = this.requestConfig.getConnectTimeout();
+ if (connectTimeout >= 0) {
+ builder.setConnectTimeout(connectTimeout);
+ }
+ int connectionRequestTimeout = this.requestConfig.getConnectionRequestTimeout();
+ if (connectionRequestTimeout >= 0) {
+ builder.setConnectionRequestTimeout(connectionRequestTimeout);
+ }
+ int socketTimeout = this.requestConfig.getSocketTimeout();
+ if (socketTimeout >= 0) {
+ builder.setSocketTimeout(socketTimeout);
+ }
+ return builder.build();
+ }
+
+ /**
* Create a Commons HttpMethodBase object for the given HTTP method and URI specification.
* @param httpMethod the HTTP method
* @param uri the URI
@@ -286,7 +335,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
*/
@Override
public void destroy() throws Exception {
- this.httpClient.close();
+ if (this.httpClient instanceof Closeable) {
+ ((Closeable) this.httpClient).close();
+ }
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java
index 363f8e74..3ef65b0e 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java
@@ -16,16 +16,17 @@
package org.springframework.http.client;
-import java.io.ByteArrayInputStream;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
-import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpHeaders;
+import org.springframework.util.StreamUtils;
/**
* {@link org.springframework.http.client.ClientHttpResponse} implementation that uses
@@ -42,12 +43,12 @@ import org.springframework.http.HttpHeaders;
*/
final class HttpComponentsClientHttpResponse extends AbstractClientHttpResponse {
- private final CloseableHttpResponse httpResponse;
+ private final HttpResponse httpResponse;
private HttpHeaders headers;
- HttpComponentsClientHttpResponse(CloseableHttpResponse httpResponse) {
+ HttpComponentsClientHttpResponse(HttpResponse httpResponse) {
this.httpResponse = httpResponse;
}
@@ -76,7 +77,7 @@ final class HttpComponentsClientHttpResponse extends AbstractClientHttpResponse
@Override
public InputStream getBody() throws IOException {
HttpEntity entity = this.httpResponse.getEntity();
- return (entity != null ? entity.getContent() : new ByteArrayInputStream(new byte[0]));
+ return (entity != null ? entity.getContent() : StreamUtils.emptyInput());
}
@Override
@@ -88,7 +89,9 @@ final class HttpComponentsClientHttpResponse extends AbstractClientHttpResponse
EntityUtils.consume(this.httpResponse.getEntity());
}
finally {
- this.httpResponse.close();
+ if (this.httpResponse instanceof Closeable) {
+ ((Closeable) this.httpResponse).close();
+ }
}
}
catch (IOException ex) {
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java
index ddd96ce2..68756d71 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -24,9 +24,9 @@ import java.net.URI;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
-import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
@@ -47,7 +47,7 @@ import org.springframework.http.StreamingHttpOutputMessage;
*/
final class HttpComponentsStreamingClientHttpRequest extends AbstractClientHttpRequest implements StreamingHttpOutputMessage {
- private final CloseableHttpClient httpClient;
+ private final HttpClient httpClient;
private final HttpUriRequest httpRequest;
@@ -56,7 +56,7 @@ final class HttpComponentsStreamingClientHttpRequest extends AbstractClientHttpR
private Body body;
- HttpComponentsStreamingClientHttpRequest(CloseableHttpClient httpClient, HttpUriRequest httpRequest, HttpContext httpContext) {
+ HttpComponentsStreamingClientHttpRequest(HttpClient httpClient, HttpUriRequest httpRequest, HttpContext httpContext) {
this.httpClient = httpClient;
this.httpRequest = httpRequest;
this.httpContext = httpContext;
@@ -65,7 +65,7 @@ final class HttpComponentsStreamingClientHttpRequest extends AbstractClientHttpR
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.httpRequest.getMethod());
+ return HttpMethod.resolve(this.httpRequest.getMethod());
}
@Override
@@ -94,7 +94,7 @@ final class HttpComponentsStreamingClientHttpRequest extends AbstractClientHttpR
entityEnclosingRequest.setEntity(requestEntity);
}
- CloseableHttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
+ HttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
return new HttpComponentsClientHttpResponse(httpResponse);
}
diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java
index 2128c2bd..929cfb62 100644
--- a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequest.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.
@@ -42,12 +42,13 @@ import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.SettableListenableFuture;
/**
- * {@link org.springframework.http.client.ClientHttpRequest} implementation that uses
- * Netty 4 to execute requests.
+ * {@link org.springframework.http.client.ClientHttpRequest} implementation
+ * that uses Netty 4 to execute requests.
*
* <p>Created via the {@link Netty4ClientHttpRequestFactory}.
*
* @author Arjen Poutsma
+ * @author Rossen Stoyanchev
* @since 4.1.2
*/
class Netty4ClientHttpRequest extends AbstractAsyncClientHttpRequest implements ClientHttpRequest {
@@ -61,11 +62,11 @@ class Netty4ClientHttpRequest extends AbstractAsyncClientHttpRequest implements
private final ByteBufOutputStream body;
- public Netty4ClientHttpRequest(Bootstrap bootstrap, URI uri, HttpMethod method, int maxRequestSize) {
+ public Netty4ClientHttpRequest(Bootstrap bootstrap, URI uri, HttpMethod method) {
this.bootstrap = bootstrap;
this.uri = uri;
this.method = method;
- this.body = new ByteBufOutputStream(Unpooled.buffer(1024, maxRequestSize));
+ this.body = new ByteBufOutputStream(Unpooled.buffer(1024));
}
@@ -147,8 +148,8 @@ class Netty4ClientHttpRequest extends AbstractAsyncClientHttpRequest implements
FullHttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
nettyMethod, this.uri.toString(), this.body.buffer());
- nettyRequest.headers().set(HttpHeaders.HOST, uri.getHost());
- nettyRequest.headers().set(HttpHeaders.CONNECTION, io.netty.handler.codec.http.HttpHeaders.Values.CLOSE);
+ nettyRequest.headers().set(HttpHeaders.HOST, this.uri.getHost());
+ nettyRequest.headers().set(HttpHeaders.CONNECTION, "close");
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
nettyRequest.headers().add(entry.getKey(), entry.getValue());
diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java
index 6225696e..2f8f0d1e 100644
--- a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpRequestFactory.java
@@ -18,17 +18,21 @@ package org.springframework.http.client;
import java.io.IOException;
import java.net.URI;
+import java.util.concurrent.TimeUnit;
import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.SocketChannelConfig;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslContext;
+import io.netty.handler.timeout.ReadTimeoutHandler;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
@@ -50,14 +54,6 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
AsyncClientHttpRequestFactory, InitializingBean, DisposableBean {
/**
- * The default maximum request size.
- * @see #setMaxRequestSize(int)
- * @deprecated
- */
- @Deprecated
- public static final int DEFAULT_MAX_REQUEST_SIZE = 1024 * 1024 * 10;
-
- /**
* The default maximum response size.
* @see #setMaxResponseSize(int)
*/
@@ -68,12 +64,14 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
private final boolean defaultEventLoopGroup;
- private int maxRequestSize = DEFAULT_MAX_REQUEST_SIZE;
-
- private int maxResponseSize = DEFAULT_MAX_REQUEST_SIZE;
+ private int maxResponseSize = DEFAULT_MAX_RESPONSE_SIZE;
private SslContext sslContext;
+ private int connectTimeout = -1;
+
+ private int readTimeout = -1;
+
private volatile Bootstrap bootstrap;
@@ -102,18 +100,6 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
/**
- * Set the default maximum request size.
- * <p>By default this is set to {@link #DEFAULT_MAX_REQUEST_SIZE}.
- * @see HttpObjectAggregator#HttpObjectAggregator(int)
- * @deprecated as of 4.1.5 this property is no longer supported;
- * effectively renamed to {@link #setMaxResponseSize(int)}.
- */
- @Deprecated
- public void setMaxRequestSize(int maxRequestSize) {
- this.maxRequestSize = maxRequestSize;
- }
-
- /**
* Set the default maximum response size.
* <p>By default this is set to {@link #DEFAULT_MAX_RESPONSE_SIZE}.
* @see HttpObjectAggregator#HttpObjectAggregator(int)
@@ -123,7 +109,6 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
this.maxResponseSize = maxResponseSize;
}
-
/**
* Set the SSL context. When configured it is used to create and insert an
* {@link io.netty.handler.ssl.SslHandler} in the channel pipeline.
@@ -133,6 +118,24 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
this.sslContext = sslContext;
}
+ /**
+ * Set the underlying connect timeout (in milliseconds).
+ * A timeout value of 0 specifies an infinite timeout.
+ * @see ChannelConfig#setConnectTimeoutMillis(int)
+ */
+ public void setConnectTimeout(int connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ }
+
+ /**
+ * Set the underlying URLConnection's read timeout (in milliseconds).
+ * A timeout value of 0 specifies an infinite timeout.
+ * @see ReadTimeoutHandler
+ */
+ public void setReadTimeout(int readTimeout) {
+ this.readTimeout = readTimeout;
+ }
+
private Bootstrap getBootstrap() {
if (this.bootstrap == null) {
Bootstrap bootstrap = new Bootstrap();
@@ -140,12 +143,17 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
+ configureChannel(channel.config());
ChannelPipeline pipeline = channel.pipeline();
if (sslContext != null) {
pipeline.addLast(sslContext.newHandler(channel.alloc()));
}
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(maxResponseSize));
+ if (readTimeout > 0) {
+ pipeline.addLast(new ReadTimeoutHandler(readTimeout,
+ TimeUnit.MILLISECONDS));
+ }
}
});
this.bootstrap = bootstrap;
@@ -153,6 +161,17 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
return this.bootstrap;
}
+ /**
+ * Template method for changing properties on the given {@link SocketChannelConfig}.
+ * <p>The default implementation sets the connect timeout based on the set property.
+ * @param config the channel configuration
+ */
+ protected void configureChannel(SocketChannelConfig config) {
+ if (this.connectTimeout >= 0) {
+ config.setConnectTimeoutMillis(this.connectTimeout);
+ }
+ }
+
@Override
public void afterPropertiesSet() {
getBootstrap();
@@ -170,7 +189,7 @@ public class Netty4ClientHttpRequestFactory implements ClientHttpRequestFactory,
}
private Netty4ClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) {
- return new Netty4ClientHttpRequest(getBootstrap(), uri, httpMethod, this.maxRequestSize);
+ return new Netty4ClientHttpRequest(getBootstrap(), uri, httpMethod);
}
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 0963d254..be619f1f 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
@@ -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.
@@ -28,8 +28,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
/**
- * {@link org.springframework.http.client.ClientHttpResponse} implementation that uses
- * Netty 4 to execute requests.
+ * {@link org.springframework.http.client.ClientHttpResponse} implementation
+ * that uses Netty 4 to parse responses.
*
* @author Arjen Poutsma
* @since 4.1.2
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
new file mode 100644
index 00000000..fe519f06
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.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.
+ *
+ * <p>Created via the {@link OkHttpClientHttpRequestFactory}.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @since 4.2
+ */
+class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest implements ClientHttpRequest {
+
+ private final OkHttpClient client;
+
+ private final URI uri;
+
+ private final HttpMethod method;
+
+
+ public OkHttpClientHttpRequest(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 {
+
+ 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();
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..9b2674dd
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java
@@ -0,0 +1,116 @@
+/*
+ * 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.concurrent.TimeUnit;
+
+import com.squareup.okhttp.OkHttpClient;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.Assert;
+
+/**
+ * {@link ClientHttpRequestFactory} implementation that uses
+ * <a href="http://square.github.io/okhttp/">OkHttp</a> to create requests.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @since 4.2
+ */
+public class OkHttpClientHttpRequestFactory
+ implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean {
+
+ private final OkHttpClient client;
+
+ private final boolean defaultClient;
+
+
+ /**
+ * Create a factory with a default {@link OkHttpClient} instance.
+ */
+ public OkHttpClientHttpRequestFactory() {
+ this.client = new OkHttpClient();
+ this.defaultClient = true;
+ }
+
+ /**
+ * Create a factory with the given {@link OkHttpClient} instance.
+ * @param client the client to use
+ */
+ public OkHttpClientHttpRequestFactory(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 OkHttpClient#setReadTimeout(long, TimeUnit)
+ */
+ public void setReadTimeout(int readTimeout) {
+ this.client.setReadTimeout(readTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Sets the underlying write timeout in milliseconds.
+ * A value of 0 specifies an infinite timeout.
+ * @see OkHttpClient#setWriteTimeout(long, TimeUnit)
+ */
+ public void setWriteTimeout(int writeTimeout) {
+ this.client.setWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Sets the underlying connect timeout in milliseconds.
+ * A value of 0 specifies an infinite timeout.
+ * @see OkHttpClient#setConnectTimeout(long, TimeUnit)
+ */
+ public void setConnectTimeout(int connectTimeout) {
+ this.client.setConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS);
+ }
+
+
+ @Override
+ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
+ return createRequestInternal(uri, httpMethod);
+ }
+
+ @Override
+ public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) {
+ return createRequestInternal(uri, httpMethod);
+ }
+
+ private OkHttpClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) {
+ return new OkHttpClientHttpRequest(this.client, uri, httpMethod);
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ if (this.defaultClient) {
+ // Clean up the client if we created it in the constructor
+ if (this.client.getCache() != null) {
+ this.client.getCache().close();
+ }
+ this.client.getDispatcher().getExecutorService().shutdown();
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..392a2d7d
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.squareup.okhttp.Response;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.Assert;
+
+/**
+ * {@link ClientHttpResponse} implementation based on OkHttp.
+ *
+ * @author Luciano Leggieri
+ * @author Arjen Poutsma
+ * @since 4.2
+ */
+class OkHttpClientHttpResponse extends AbstractClientHttpResponse {
+
+ private final Response response;
+
+ private HttpHeaders headers;
+
+
+ public OkHttpClientHttpResponse(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() {
+ try {
+ this.response.body().close();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+
+}
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 336cf4f2..015a2e42 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-2014 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.
@@ -57,7 +57,7 @@ final class SimpleBufferingAsyncClientHttpRequest extends AbstractBufferingAsync
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.connection.getRequestMethod());
+ return HttpMethod.resolve(this.connection.getRequestMethod());
}
@Override
@@ -78,8 +78,8 @@ final class SimpleBufferingAsyncClientHttpRequest extends AbstractBufferingAsync
@Override
public ClientHttpResponse call() throws Exception {
SimpleBufferingClientHttpRequest.addHeaders(connection, headers);
- // JDK < 1.8 doesn't support getOutputStream with HTTP DELETE
- if (HttpMethod.DELETE.equals(getMethod()) && bufferedOutput.length == 0) {
+ // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
+ if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) {
connection.setDoOutput(false);
}
if (connection.getDoOutput() && outputStreaming) {
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 0247ef6c..24195beb 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-2014 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.
@@ -26,7 +26,6 @@ import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.FileCopyUtils;
-import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -53,7 +52,7 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.connection.getRequestMethod());
+ return HttpMethod.resolve(this.connection.getRequestMethod());
}
@Override
@@ -70,8 +69,8 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp
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.equals(getMethod()) && bufferedOutput.length == 0) {
+ // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
+ if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) {
this.connection.setDoOutput(false);
}
@@ -101,7 +100,8 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp
}
else {
for (String headerValue : entry.getValue()) {
- connection.addRequestProperty(headerName, headerValue);
+ String actualHeaderValue = headerValue != null ? headerValue : "";
+ connection.addRequestProperty(headerName, actualHeaderValue);
}
}
}
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 cf6f5969..0417eef0 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-2014 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.
@@ -63,7 +63,7 @@ final class SimpleStreamingAsyncClientHttpRequest extends AbstractAsyncClientHtt
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.connection.getRequestMethod());
+ return HttpMethod.resolve(this.connection.getRequestMethod());
}
@Override
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 504f567a..5e871d00 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-2014 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.
@@ -53,7 +53,7 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest {
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.connection.getRequestMethod());
+ return HttpMethod.resolve(this.connection.getRequestMethod());
}
@Override
diff --git a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java
index aa2eded1..fa60e171 100644
--- a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java
+++ b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java
@@ -50,7 +50,8 @@ public abstract class HttpAccessor {
/**
- * Set the request factory that this accessor uses for obtaining {@link ClientHttpRequest HttpRequests}.
+ * Set the request factory that this accessor uses for obtaining
+ * {@link ClientHttpRequest HttpRequests}.
*/
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
Assert.notNull(requestFactory, "'requestFactory' must not be null");
diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java
new file mode 100644
index 00000000..a52b032a
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.converter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Type;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.StreamingHttpOutputMessage;
+
+/**
+ * Abstract base class for most {@link GenericHttpMessageConverter} implementations.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHttpMessageConverter<T>
+ implements GenericHttpMessageConverter<T> {
+
+ /**
+ * Construct an {@code AbstractGenericHttpMessageConverter} with no supported media types.
+ * @see #setSupportedMediaTypes
+ */
+ protected AbstractGenericHttpMessageConverter() {
+ }
+
+ /**
+ * Construct an {@code AbstractGenericHttpMessageConverter} with one supported media type.
+ * @param supportedMediaType the supported media type
+ */
+ protected AbstractGenericHttpMessageConverter(MediaType supportedMediaType) {
+ super(supportedMediaType);
+ }
+
+ /**
+ * Construct an {@code AbstractGenericHttpMessageConverter} with multiple supported media type.
+ * @param supportedMediaTypes the supported media types
+ */
+ protected AbstractGenericHttpMessageConverter(MediaType... supportedMediaTypes) {
+ super(supportedMediaTypes);
+ }
+
+
+ @Override
+ public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
+ return canRead(contextClass, mediaType);
+ }
+
+ @Override
+ public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
+ return canWrite(clazz, mediaType);
+ }
+
+ /**
+ * This implementation sets the default headers by calling {@link #addDefaultHeaders},
+ * and then calls {@link #writeInternal}.
+ */
+ public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException {
+
+ final HttpHeaders headers = outputMessage.getHeaders();
+ addDefaultHeaders(headers, t, contentType);
+
+ if (outputMessage instanceof StreamingHttpOutputMessage) {
+ StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
+ streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
+ @Override
+ public void writeTo(final OutputStream outputStream) throws IOException {
+ writeInternal(t, type, new HttpOutputMessage() {
+ @Override
+ public OutputStream getBody() throws IOException {
+ return outputStream;
+ }
+ @Override
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+ });
+ }
+ });
+ }
+ else {
+ writeInternal(t, type, outputMessage);
+ outputMessage.getBody().flush();
+ }
+ }
+
+
+ @Override
+ protected void writeInternal(T t, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException {
+
+ writeInternal(t, null, outputMessage);
+ }
+
+ /**
+ * Abstract template method that writes the actual body. Invoked from {@link #write}.
+ * @param t the object to write to the output message
+ * @param type the type of object to write, can be {@code null} if not specified.
+ * @param outputMessage the HTTP output message to write to
+ * @throws IOException in case of I/O errors
+ * @throws HttpMessageNotWritableException in case of conversion errors
+ */
+ protected abstract void writeInternal(T t, Type type, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException;
+
+}
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 25d5faf5..42a91192 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
@@ -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.
@@ -68,7 +68,7 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
}
/**
- * Construct an {@code AbstractHttpMessageConverter} with multiple supported media type.
+ * Construct an {@code AbstractHttpMessageConverter} with multiple supported media types.
* @param supportedMediaTypes the supported media types
*/
protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) {
@@ -101,8 +101,9 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
}
/**
- * Returns true if any of the {@linkplain #setSupportedMediaTypes(List) supported media types}
- * include the given media type.
+ * Returns {@code true} if any of the {@linkplain #setSupportedMediaTypes(List)
+ * supported} media types {@link MediaType#includes(MediaType) include} the
+ * given media type.
* @param mediaType the media type to read, can be {@code null} if not specified.
* Typically the value of a {@code Content-Type} header.
* @return {@code true} if the supported media types include the media type,
@@ -121,14 +122,15 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
}
/**
- * This implementation checks if the given class is {@linkplain #supports(Class) supported},
- * and if the {@linkplain #getSupportedMediaTypes() supported media types}
+ * This implementation checks if the given class is
+ * {@linkplain #supports(Class) supported}, and if the
+ * {@linkplain #getSupportedMediaTypes() supported} media types
* {@linkplain MediaType#includes(MediaType) include} the given media type.
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return supports(clazz) && canWrite(mediaType);
- }
+ }
/**
* Returns {@code true} if the given media type includes any of the
@@ -160,30 +162,15 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
}
/**
- * This implementation delegates to {@link #getDefaultContentType(Object)} if a content
- * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers
- * on the output message. It then calls {@link #writeInternal}.
+ * This implementation sets the default headers by calling {@link #addDefaultHeaders},
+ * and then calls {@link #writeInternal}.
*/
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
- if (headers.getContentType() == null) {
- MediaType contentTypeToUse = contentType;
- if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
- contentTypeToUse = getDefaultContentType(t);
- }
- if (contentTypeToUse != null) {
- headers.setContentType(contentTypeToUse);
- }
- }
- if (headers.getContentLength() == -1) {
- Long contentLength = getContentLength(t, headers.getContentType());
- if (contentLength != null) {
- headers.setContentLength(contentLength);
- }
- }
+ addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
@@ -211,6 +198,34 @@ 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
+ * @since 4.2
+ */
+ protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
+ if (headers.getContentType() == null) {
+ MediaType contentTypeToUse = contentType;
+ if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
+ contentTypeToUse = getDefaultContentType(t);
+ }
+ else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
+ MediaType mediaType = getDefaultContentType(t);
+ contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
+ }
+ if (contentTypeToUse != null) {
+ headers.setContentType(contentTypeToUse);
+ }
+ }
+ if (headers.getContentLength() < 0) {
+ Long contentLength = getContentLength(t, headers.getContentType());
+ if (contentLength != null) {
+ headers.setContentLength(contentLength);
+ }
+ }
+ }
+
+ /**
* Returns the default content type for the given type. Called when {@link #write}
* is invoked without a specified content type parameter.
* <p>By default, this returns the first element of the
diff --git a/spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java
index 7df72035..359cac63 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -41,6 +41,7 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
+import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -127,7 +128,7 @@ public class BufferedImageHttpMessageConverter implements HttpMessageConverter<B
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
- return (BufferedImage.class.equals(clazz) && isReadable(mediaType));
+ return (BufferedImage.class == clazz && isReadable(mediaType));
}
private boolean isReadable(MediaType mediaType) {
@@ -140,7 +141,7 @@ public class BufferedImageHttpMessageConverter implements HttpMessageConverter<B
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
- return (BufferedImage.class.equals(clazz) && isWritable(mediaType));
+ return (BufferedImage.class == clazz && isWritable(mediaType));
}
private boolean isWritable(MediaType mediaType) {
@@ -203,24 +204,48 @@ public class BufferedImageHttpMessageConverter implements HttpMessageConverter<B
}
@Override
- public void write(BufferedImage image, MediaType contentType, HttpOutputMessage outputMessage)
+ public void write(final BufferedImage image, final MediaType contentType,
+ final HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
+ final MediaType selectedContentType = getContentType(contentType);
+ outputMessage.getHeaders().setContentType(selectedContentType);
+
+ if (outputMessage instanceof StreamingHttpOutputMessage) {
+ StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
+ streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException {
+ writeInternal(image, selectedContentType, outputStream);
+ }
+ });
+ }
+ else {
+ writeInternal(image, selectedContentType, outputMessage.getBody());
+ }
+ }
+
+ private MediaType getContentType(MediaType contentType) {
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = getDefaultContentType();
}
- Assert.notNull(contentType,
- "Count not determine Content-Type, set one using the 'defaultContentType' property");
- outputMessage.getHeaders().setContentType(contentType);
+ Assert.notNull(contentType, "Could not select Content-Type. " +
+ "Please specify one through the 'defaultContentType' property.");
+ return contentType;
+ }
+
+ private void writeInternal(BufferedImage image, MediaType contentType, OutputStream body)
+ throws IOException, HttpMessageNotWritableException {
+
ImageOutputStream imageOutputStream = null;
ImageWriter imageWriter = null;
try {
- imageOutputStream = createImageOutputStream(outputMessage.getBody());
Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(contentType.toString());
if (imageWriters.hasNext()) {
imageWriter = imageWriters.next();
ImageWriteParam iwp = imageWriter.getDefaultWriteParam();
process(iwp);
+ imageOutputStream = createImageOutputStream(body);
imageWriter.setOutput(imageOutputStream);
imageWriter.write(null, new IIOImage(image, null, null), iwp);
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java
index 800a9feb..87c003d7 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java
@@ -43,7 +43,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
@Override
public boolean supports(Class<?> clazz) {
- return byte[].class.equals(clazz);
+ return byte[].class == clazz;
}
@Override
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 919407d9..44491be0 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -27,7 +27,6 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Random;
import javax.mail.internet.MimeUtility;
import org.springframework.core.io.Resource;
@@ -36,8 +35,10 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
+import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
@@ -88,12 +89,6 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
- private static final byte[] BOUNDARY_CHARS =
- new byte[] {'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
- 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
- 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
- 'V', 'W', 'X', 'Y', 'Z'};
-
private Charset charset = DEFAULT_CHARSET;
@@ -103,8 +98,6 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
- private final Random random = new Random();
-
public FormHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
@@ -256,8 +249,8 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
return false;
}
- private void writeForm(MultiValueMap<String, String> form, MediaType contentType, HttpOutputMessage outputMessage)
- throws IOException {
+ private void writeForm(MultiValueMap<String, String> form, MediaType contentType,
+ HttpOutputMessage outputMessage) throws IOException {
Charset charset;
if (contentType != null) {
@@ -286,20 +279,45 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
builder.append('&');
}
}
- byte[] bytes = builder.toString().getBytes(charset.name());
+ final byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
- StreamUtils.copy(bytes, outputMessage.getBody());
+
+ if (outputMessage instanceof StreamingHttpOutputMessage) {
+ StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
+ streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException {
+ StreamUtils.copy(bytes, outputStream);
+ }
+ });
+ }
+ else {
+ StreamUtils.copy(bytes, outputMessage.getBody());
+ }
}
- private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
- byte[] boundary = generateMultipartBoundary();
+ private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
+ final byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
- outputMessage.getHeaders().setContentType(contentType);
-
- writeParts(outputMessage.getBody(), parts, boundary);
- writeEnd(outputMessage.getBody(), boundary);
+ HttpHeaders headers = outputMessage.getHeaders();
+ headers.setContentType(contentType);
+
+ if (outputMessage instanceof StreamingHttpOutputMessage) {
+ StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
+ streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException {
+ writeParts(outputStream, parts, boundary);
+ writeEnd(outputStream, boundary);
+ }
+ });
+ }
+ else {
+ writeParts(outputMessage.getBody(), parts, boundary);
+ writeEnd(outputMessage.getBody(), boundary);
+ }
}
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
@@ -339,15 +357,11 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
/**
* Generate a multipart boundary.
- * <p>The default implementation returns a random boundary.
- * Can be overridden in subclasses.
+ * <p>This implementation delegates to
+ * {@link MimeTypeUtils#generateMultipartBoundary()}.
*/
protected byte[] generateMultipartBoundary() {
- byte[] boundary = new byte[this.random.nextInt(11) + 30];
- for (int i = 0; i < boundary.length; i++) {
- boundary[i] = BOUNDARY_CHARS[this.random.nextInt(BOUNDARY_CHARS.length)];
- }
- return boundary;
+ return MimeTypeUtils.generateMultipartBoundary();
}
/**
diff --git a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java
index 27600b71..300783b2 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java
@@ -20,14 +20,17 @@ import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
/**
- * A specialization of {@link HttpMessageConverter} that can convert an HTTP
- * request into a target object of a specified generic type.
+ * A specialization of {@link HttpMessageConverter} that can convert an HTTP request
+ * into a target object of a specified generic type and a source object of a specified
+ * generic type into an HTTP response.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Sebastien Deleuze
* @since 3.2
* @see org.springframework.core.ParameterizedTypeReference
*/
@@ -35,7 +38,10 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
/**
* Indicates whether the given type can be read by this converter.
- * @param type the type to test for readability
+ * This method should perform the same checks than
+ * {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones
+ * related to the generic type.
+ * @param type the (potentially generic) type to test for readability
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature (can be {@code null})
* @param mediaType the media type to read, can be {@code null} if not specified.
@@ -46,8 +52,8 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
/**
* Read an object of the given type form the given input message, and returns it.
- * @param type the type of object to return. This type must have previously
- * been passed to the {@link #canRead canRead} method of this interface,
+ * @param type the (potentially generic) type of object to return. This type must have
+ * previously been passed to the {@link #canRead canRead} method of this interface,
* which must have returned {@code true}.
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature (can be {@code null})
@@ -59,4 +65,40 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
+ /**
+ * Indicates whether the given class can be written by this converter.
+ * This method should perform the same checks than
+ * {@link HttpMessageConverter#canWrite(Class, MediaType)} with additional ones
+ * related to the generic type.
+ * @param type the (potentially generic) type to test for writability, can be
+ * {@code null} if not specified.
+ * @param clazz the source object class to test for writability
+ * @param mediaType the media type to write, can be {@code null} if not specified.
+ * Typically the value of an {@code Accept} header.
+ * @return {@code true} if writable; {@code false} otherwise
+ * @since 4.2
+ */
+ boolean canWrite(Type type, Class<?> clazz, MediaType mediaType);
+
+ /**
+ * Write an given object to the given output message.
+ * @param t the object to write to the output message. The type of this object must
+ * have previously been passed to the {@link #canWrite canWrite} method of this
+ * interface, which must have returned {@code true}.
+ * @param type the (potentially generic) type of object to write. This type must have
+ * previously been passed to the {@link #canWrite canWrite} method of this interface,
+ * which must have returned {@code true}. Can be {@code null} if not specified.
+ * @param contentType the content type to use when writing. May be {@code null} to
+ * indicate that the default content type of the converter must be used. If not
+ * {@code null}, this media type must have previously been passed to the
+ * {@link #canWrite canWrite} method of this interface, which must have returned
+ * {@code true}.
+ * @param outputMessage the message to write to
+ * @throws IOException in case of I/O errors
+ * @throws HttpMessageNotWritableException in case of conversion errors
+ * @since 4.2
+ */
+ void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException;
+
}
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 4429d4d5..cc8da360 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
@@ -40,6 +40,8 @@ import org.springframework.util.StringUtils;
* If JAF is not available, {@code application/octet-stream} is used.
*
* @author Arjen Poutsma
+ * @author Juergen Hoeller
+ * @author Kazuki Shimizu
* @since 3.0.2
*/
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
@@ -62,8 +64,16 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
- byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
- return new ByteArrayResource(body);
+ if (InputStreamResource.class == clazz){
+ return new InputStreamResource(inputMessage.getBody());
+ }
+ else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
+ byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
+ return new ByteArrayResource(body);
+ }
+ else {
+ throw new IllegalStateException("Unsupported resource class: " + clazz);
+ }
}
@Override
@@ -80,7 +90,7 @@ 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.equals(resource.getClass()) ? null : resource.contentLength());
+ return (InputStreamResource.class == resource.getClass() ? null : resource.contentLength());
}
@Override
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 4e78c3c7..15c2693c 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
@@ -79,7 +79,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
@Override
public boolean supports(Class<?> clazz) {
- return String.class.equals(clazz);
+ return String.class == clazz;
}
@Override
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 eff7b31b..f29dc806 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
@@ -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,27 +24,31 @@ 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.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.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
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 and higher.
+ * <p>Compatible with Jackson 2.1 to 2.6.
*
* @author Arjen Poutsma
* @author Keith Donald
@@ -53,8 +57,7 @@ import org.springframework.util.ClassUtils;
* @author Sebastien Deleuze
* @since 4.1
*/
-public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
- implements GenericHttpMessageConverter<Object> {
+public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -62,6 +65,10 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
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;
@@ -135,17 +142,20 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
+ if (!canRead(mediaType)) {
+ return false;
+ }
JavaType javaType = getJavaType(type, contextClass);
if (!jackson23Available || !logger.isWarnEnabled()) {
- return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
+ return this.objectMapper.canDeserialize(javaType);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
- if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) {
+ if (this.objectMapper.canDeserialize(javaType, causeRef)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
- String msg = "Failed to evaluate deserialization for type " + javaType;
+ String msg = "Failed to evaluate Jackson deserialization for type " + javaType;
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
@@ -158,16 +168,19 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
+ if (!canWrite(mediaType)) {
+ return false;
+ }
if (!jackson23Available || !logger.isWarnEnabled()) {
- return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
+ return this.objectMapper.canSerialize(clazz);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
- if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) {
+ if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
- String msg = "Failed to evaluate serialization for type [" + clazz + "]";
+ String msg = "Failed to evaluate Jackson serialization for type [" + clazz + "]";
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
@@ -200,8 +213,16 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
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).
+ readValue(inputMessage.getBody());
+ }
+ }
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (IOException ex) {
@@ -210,26 +231,43 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
}
@Override
- protected void writeInternal(Object object, HttpOutputMessage outputMessage)
+ @SuppressWarnings("deprecation")
+ protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
+
Class<?> serializationView = null;
+ FilterProvider filters = null;
Object value = object;
- if (value instanceof MappingJacksonValue) {
+ JavaType javaType = null;
+ if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
+ filters = container.getFilters();
+ }
+ if (jackson26Available && type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
+ javaType = getJavaType(type, null);
}
+ ObjectWriter objectWriter;
if (serializationView != null) {
- this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
+ objectWriter = this.objectMapper.writerWithView(serializationView);
+ }
+ else if (filters != null) {
+ objectWriter = this.objectMapper.writer(filters);
}
else {
- this.objectMapper.writeValue(generator, value);
+ objectWriter = this.objectMapper.writer();
}
+ if (javaType != null && javaType.isContainerType()) {
+ objectWriter = objectWriter.withType(javaType);
+ }
+ objectWriter.writeValue(generator, value);
+
writeSuffix(generator, object);
generator.flush();
@@ -275,7 +313,10 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
* @return the Jackson JavaType
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
- return this.objectMapper.getTypeFactory().constructType(type, 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));
}
/**
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonBuilderUtils.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonBuilderUtils.java
index 881f0f59..54aa0e08 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonBuilderUtils.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonBuilderUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -52,10 +52,6 @@ public abstract class GsonBuilderUtils {
* On Java 8, the standard {@link java.util.Base64} facility is used instead.
*/
public static GsonBuilder gsonBuilderWithBase64EncodedByteArrays() {
- // Assert that Base64 support is available, as long we're not on Java 8+
- Base64Utils.encode(null);
-
- // Now, construct a pre-configured GsonBuilder...
GsonBuilder builder = new GsonBuilder();
builder.registerTypeHierarchyAdapter(byte[].class, new Base64TypeAdapter());
return builder;
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 be5b4ea6..38f7f432 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
@@ -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.
@@ -32,8 +32,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
@@ -45,17 +44,17 @@ import org.springframework.util.Assert;
* {@link Gson} class.
*
* <p>This converter can be used to bind to typed beans or untyped {@code HashMap}s.
- * By default, it supports {@code application/json} and {@code application/*+json}.
+ * By default, it supports {@code application/json} and {@code application/*+json} with
+ * {@code UTF-8} character set.
*
- * <p>Tested against Gson 2.3; compatible with Gson 2.0 and higher.
+ * <p>Tested against Gson 2.6; compatible with Gson 2.0 and higher.
*
* @author Roy Clarkson
* @since 4.1
* @see #setGson
* @see #setSupportedMediaTypes
*/
-public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
- implements GenericHttpMessageConverter<Object> {
+public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -69,8 +68,7 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
* Construct a new {@code GsonHttpMessageConverter}.
*/
public GsonHttpMessageConverter() {
- super(new MediaType("application", "json", DEFAULT_CHARSET),
- new MediaType("application", "*+json", DEFAULT_CHARSET));
+ super(MediaType.APPLICATION_JSON_UTF8, new MediaType("application", "*+json", DEFAULT_CHARSET));
}
@@ -101,17 +99,16 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
}
/**
- * Indicate whether the JSON output by this view should be prefixed with "{} &&".
+ * Indicate whether the JSON output by this view should be prefixed with ")]}', ".
* Default is {@code false}.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON
* Hijacking. The prefix renders the string syntactically invalid as a script
- * so that it cannot be hijacked. This prefix does not affect the evaluation
- * of JSON, but if JSON validation is performed on the string, the prefix
- * would need to be ignored.
+ * so that it cannot be hijacked.
+ * This prefix should be stripped before parsing the string as JSON.
* @see #setJsonPrefix
*/
public void setPrefixJson(boolean prefixJson) {
- this.jsonPrefix = (prefixJson ? "{} && " : null);
+ this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
@@ -121,11 +118,6 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
}
@Override
- public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
- return canRead(mediaType);
- }
-
- @Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return canWrite(mediaType);
}
@@ -192,7 +184,7 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
}
@Override
- protected void writeInternal(Object o, HttpOutputMessage outputMessage)
+ protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Charset charset = getCharset(outputMessage.getHeaders());
@@ -201,7 +193,12 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
if (this.jsonPrefix != null) {
writer.append(this.jsonPrefix);
}
- this.gson.toJson(o, writer);
+ if (type != null) {
+ this.gson.toJson(o, type, writer);
+ }
+ else {
+ this.gson.toJson(o, writer);
+ }
writer.close();
}
catch (JsonIOException ex) {
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 290c32a4..b2ee1e5b 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
@@ -16,7 +16,6 @@
package org.springframework.http.converter.json;
-import java.io.ByteArrayInputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
@@ -30,6 +29,7 @@ import java.util.TimeZone;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLResolver;
+import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@@ -44,7 +44,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
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.XmlMapper;
import org.springframework.beans.BeanUtils;
@@ -52,6 +54,7 @@ import org.springframework.beans.FatalBeanException;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
@@ -63,10 +66,16 @@ import org.springframework.util.StringUtils;
* <li>{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled</li>
* </ul>
*
- * <p>Note that Jackson's JSR-310 and Joda-Time support modules will be registered automatically
- * when available (and when Java 8 and Joda-Time themselves are available, respectively).
+ * <p>It also automatically registers the following well-known modules if they are
+ * detected on the classpath:
+ * <ul>
+ * <li><a href="https://github.com/FasterXML/jackson-datatype-jdk7">jackson-datatype-jdk7</a>: support for Java 7 types like {@link java.nio.file.Path}</li>
+ * <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>
+ * </ul>
*
- * <p>Tested against Jackson 2.2, 2.3, 2.4 and 2.5; compatible with Jackson 2.0 and higher.
+ * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher.
*
* @author Sebastien Deleuze
* @author Juergen Hoeller
@@ -90,14 +99,18 @@ public class Jackson2ObjectMapperBuilder {
private PropertyNamingStrategy propertyNamingStrategy;
+ private TypeResolverBuilder<?> defaultTyping;
+
private JsonInclude.Include serializationInclusion;
+ private FilterProvider filters;
+
+ private final Map<Class<?>, Class<?>> mixIns = new HashMap<Class<?>, Class<?>>();
+
private final Map<Class<?>, JsonSerializer<?>> serializers = new LinkedHashMap<Class<?>, JsonSerializer<?>>();
private final Map<Class<?>, JsonDeserializer<?>> deserializers = new LinkedHashMap<Class<?>, JsonDeserializer<?>>();
- private final Map<Class<?>, Class<?>> mixIns = new HashMap<Class<?>, Class<?>>();
-
private final Map<Object, Boolean> features = new HashMap<Object, Boolean>();
private List<Module> modules;
@@ -207,6 +220,15 @@ public class Jackson2ObjectMapperBuilder {
}
/**
+ * Specify a {@link TypeResolverBuilder} to use for Jackson's default typing.
+ * @since 4.2.2
+ */
+ public Jackson2ObjectMapperBuilder defaultTyping(TypeResolverBuilder<?> typeResolverBuilder) {
+ this.defaultTyping = typeResolverBuilder;
+ return this;
+ }
+
+ /**
* Set a custom inclusion strategy for serialization.
* @see com.fasterxml.jackson.annotation.JsonInclude.Include
*/
@@ -216,6 +238,46 @@ public class Jackson2ObjectMapperBuilder {
}
/**
+ * Set the global filters to use in order to support {@link JsonFilter @JsonFilter} annotated POJO.
+ * @since 4.2
+ * @see MappingJacksonValue#setFilters(FilterProvider)
+ */
+ public Jackson2ObjectMapperBuilder filters(FilterProvider filters) {
+ this.filters = filters;
+ return this;
+ }
+
+ /**
+ * Add mix-in annotations to use for augmenting specified class or interface.
+ * @param target class (or interface) whose annotations to effectively override
+ * @param mixinSource class (or interface) whose annotations are to be "added"
+ * to target's annotations as value
+ * @since 4.1.2
+ * @see com.fasterxml.jackson.databind.ObjectMapper#addMixInAnnotations(Class, Class)
+ */
+ public Jackson2ObjectMapperBuilder mixIn(Class<?> target, Class<?> mixinSource) {
+ if (mixinSource != null) {
+ this.mixIns.put(target, mixinSource);
+ }
+ return this;
+ }
+
+ /**
+ * Add mix-in annotations to use for augmenting specified class or interface.
+ * @param mixIns Map of entries with target classes (or interface) whose annotations
+ * to effectively override as key and mix-in classes (or interface) whose
+ * annotations are to be "added" to target's annotations as value.
+ * @since 4.1.2
+ * @see com.fasterxml.jackson.databind.ObjectMapper#addMixInAnnotations(Class, Class)
+ */
+ public Jackson2ObjectMapperBuilder mixIns(Map<Class<?>, Class<?>> mixIns) {
+ if (mixIns != null) {
+ this.mixIns.putAll(mixIns);
+ }
+ return this;
+ }
+
+ /**
* Configure custom serializers. Each serializer is registered for the type
* returned by {@link JsonSerializer#handledType()}, which must not be
* {@code null}.
@@ -279,36 +341,6 @@ public class Jackson2ObjectMapperBuilder {
}
/**
- * Add mix-in annotations to use for augmenting specified class or interface.
- * @param target class (or interface) whose annotations to effectively override
- * @param mixinSource class (or interface) whose annotations are to be "added"
- * to target's annotations as value
- * @since 4.1.2
- * @see com.fasterxml.jackson.databind.ObjectMapper#addMixInAnnotations(Class, Class)
- */
- public Jackson2ObjectMapperBuilder mixIn(Class<?> target, Class<?> mixinSource) {
- if (mixinSource != null) {
- this.mixIns.put(target, mixinSource);
- }
- return this;
- }
-
- /**
- * Add mix-in annotations to use for augmenting specified class or interface.
- * @param mixIns Map of entries with target classes (or interface) whose annotations
- * to effectively override as key and mix-in classes (or interface) whose
- * annotations are to be "added" to target's annotations as value.
- * @since 4.1.2
- * @see com.fasterxml.jackson.databind.ObjectMapper#addMixInAnnotations(Class, Class)
- */
- public Jackson2ObjectMapperBuilder mixIns(Map<Class<?>, Class<?>> mixIns) {
- if (mixIns != null) {
- this.mixIns.putAll(mixIns);
- }
- return this;
- }
-
- /**
* Shortcut for {@link MapperFeature#AUTO_DETECT_FIELDS} option.
*/
public Jackson2ObjectMapperBuilder autoDetectFields(boolean autoDetectFields) {
@@ -318,11 +350,13 @@ public class Jackson2ObjectMapperBuilder {
/**
* Shortcut for {@link MapperFeature#AUTO_DETECT_SETTERS}/
- * {@link MapperFeature#AUTO_DETECT_GETTERS} option.
+ * {@link MapperFeature#AUTO_DETECT_GETTERS}/{@link MapperFeature#AUTO_DETECT_IS_GETTERS}
+ * options.
*/
public Jackson2ObjectMapperBuilder autoDetectGettersSetters(boolean autoDetectGettersSetters) {
this.features.put(MapperFeature.AUTO_DETECT_GETTERS, autoDetectGettersSetters);
this.features.put(MapperFeature.AUTO_DETECT_SETTERS, autoDetectGettersSetters);
+ this.features.put(MapperFeature.AUTO_DETECT_IS_GETTERS, autoDetectGettersSetters);
return this;
}
@@ -567,10 +601,23 @@ public class Jackson2ObjectMapperBuilder {
if (this.propertyNamingStrategy != null) {
objectMapper.setPropertyNamingStrategy(this.propertyNamingStrategy);
}
+ if (this.defaultTyping != null) {
+ objectMapper.setDefaultTyping(this.defaultTyping);
+ }
if (this.serializationInclusion != null) {
objectMapper.setSerializationInclusion(this.serializationInclusion);
}
+ if (this.filters != null) {
+ // Deprecated as of Jackson 2.6, but just in favor of a fluent variant.
+ objectMapper.setFilters(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));
+ }
+
if (!this.serializers.isEmpty() || !this.deserializers.isEmpty()) {
SimpleModule module = new SimpleModule();
addSerializers(module);
@@ -583,11 +630,6 @@ public class Jackson2ObjectMapperBuilder {
configureFeature(objectMapper, feature, this.features.get(feature));
}
- 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));
- }
-
if (this.handlerInstantiator != null) {
objectMapper.setHandlerInstantiator(this.handlerInstantiator);
}
@@ -597,6 +639,7 @@ public class Jackson2ObjectMapperBuilder {
}
}
+
// Any change to this method should be also applied to spring-jms and spring-messaging
// MappingJackson2MessageConverter default constructors
private void customizeDefaultFeatures(ObjectMapper objectMapper) {
@@ -645,17 +688,50 @@ public class Jackson2ObjectMapperBuilder {
@SuppressWarnings("unchecked")
private void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper) {
+ // Java 7 java.nio.file.Path class present?
+ if (ClassUtils.isPresent("java.nio.file.Path", this.moduleClassLoader)) {
+ try {
+ Class<? extends Module> jdk7Module = (Class<? extends Module>)
+ ClassUtils.forName("com.fasterxml.jackson.datatype.jdk7.Jdk7Module", this.moduleClassLoader);
+ objectMapper.registerModule(BeanUtils.instantiate(jdk7Module));
+ }
+ catch (ClassNotFoundException ex) {
+ // jackson-datatype-jdk7 not available
+ }
+ }
+
+ // Java 8 java.util.Optional class present?
+ if (ClassUtils.isPresent("java.util.Optional", this.moduleClassLoader)) {
+ try {
+ Class<? extends Module> jdk8Module = (Class<? extends Module>)
+ ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader);
+ objectMapper.registerModule(BeanUtils.instantiate(jdk8Module));
+ }
+ catch (ClassNotFoundException ex) {
+ // jackson-datatype-jdk8 not available
+ }
+ }
+
// Java 8 java.time package present?
if (ClassUtils.isPresent("java.time.LocalDate", this.moduleClassLoader)) {
try {
- Class<? extends Module> jsr310Module = (Class<? extends Module>)
- ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JSR310Module", this.moduleClassLoader);
- objectMapper.registerModule(BeanUtils.instantiate(jsr310Module));
+ Class<? extends Module> javaTimeModule = (Class<? extends Module>)
+ ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader);
+ objectMapper.registerModule(BeanUtils.instantiate(javaTimeModule));
}
catch (ClassNotFoundException ex) {
- // jackson-datatype-jsr310 not available
+ // 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...
+ }
}
}
+
// Joda-Time present?
if (ClassUtils.isPresent("org.joda.time.LocalDate", this.moduleClassLoader)) {
try {
@@ -702,7 +778,7 @@ public class Jackson2ObjectMapperBuilder {
private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() {
@Override
public Object resolveEntity(String publicID, String systemID, String base, String ns) {
- return new ByteArrayInputStream(new byte[0]);
+ return StreamUtils.emptyInput();
}
};
}
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 b06202ae..f17016e9 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
@@ -23,6 +23,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
+import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -35,6 +36,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
+import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
+import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.beans.factory.BeanClassLoaderAware;
@@ -106,6 +109,15 @@ import org.springframework.context.ApplicationContextAware;
* &lt;/bean>
* </pre>
*
+ * <p>It also automatically registers the following well-known modules if they are
+ * detected on the classpath:
+ * <ul>
+ * <li><a href="https://github.com/FasterXML/jackson-datatype-jdk7">jackson-datatype-jdk7</a>: support for Java 7 types like {@link java.nio.file.Path}</li>
+ * <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>
+ * </ul>
+ *
* <p>In case you want to configure Jackson's {@link ObjectMapper} with a custom {@link Module},
* you can register one or more such Modules by class name via {@link #setModulesToInstall}:
*
@@ -115,10 +127,7 @@ import org.springframework.context.ApplicationContextAware;
* &lt;/bean
* </pre>
*
- * Note that Jackson's JSR-310 and Joda-Time support modules will be registered automatically
- * when available (and when Java 8 and Joda-Time themselves are available, respectively).
- *
- * <p>Tested against Jackson 2.2, 2.3, 2.4 and 2.5; compatible with Jackson 2.0 and higher.
+ * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher.
*
* @author <a href="mailto:dmitry.katsubo@gmail.com">Dmitry Katsubo</a>
* @author Rossen Stoyanchev
@@ -208,6 +217,14 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
}
/**
+ * Specify a {@link TypeResolverBuilder} to use for Jackson's default typing.
+ * @since 4.2.2
+ */
+ public void setDefaultTyping(TypeResolverBuilder<?> typeResolverBuilder) {
+ this.builder.defaultTyping(typeResolverBuilder);
+ }
+
+ /**
* Set a custom inclusion strategy for serialization.
* @see com.fasterxml.jackson.annotation.JsonInclude.Include
*/
@@ -216,6 +233,27 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
}
/**
+ * Set the global filters to use in order to support {@link JsonFilter @JsonFilter} annotated POJO.
+ * @since 4.2
+ * @see Jackson2ObjectMapperBuilder#filters(FilterProvider)
+ */
+ public void setFilters(FilterProvider filters) {
+ this.builder.filters(filters);
+ }
+
+ /**
+ * Add mix-in annotations to use for augmenting specified class or interface.
+ * @param mixIns Map of entries with target classes (or interface) whose annotations
+ * to effectively override as key and mix-in classes (or interface) whose
+ * annotations are to be "added" to target's annotations as value.
+ * @since 4.1.2
+ * @see com.fasterxml.jackson.databind.ObjectMapper#addMixInAnnotations(Class, Class)
+ */
+ public void setMixIns(Map<Class<?>, Class<?>> mixIns) {
+ this.builder.mixIns(mixIns);
+ }
+
+ /**
* Configure custom serializers. Each serializer is registered for the type
* returned by {@link JsonSerializer#handledType()}, which must not be
* {@code null}.
@@ -241,18 +279,6 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
}
/**
- * Add mix-in annotations to use for augmenting specified class or interface.
- * @param mixIns Map of entries with target classes (or interface) whose annotations
- * to effectively override as key and mix-in classes (or interface) whose
- * annotations are to be "added" to target's annotations as value.
- * @since 4.1.2
- * @see com.fasterxml.jackson.databind.ObjectMapper#addMixInAnnotations(Class, Class)
- */
- public void setMixIns(Map<Class<?>, Class<?>> mixIns) {
- this.builder.mixIns(mixIns);
- }
-
- /**
* Shortcut for {@link MapperFeature#AUTO_DETECT_FIELDS} option.
*/
public void setAutoDetectFields(boolean autoDetectFields) {
@@ -261,7 +287,8 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
/**
* Shortcut for {@link MapperFeature#AUTO_DETECT_SETTERS}/
- * {@link MapperFeature#AUTO_DETECT_GETTERS} option.
+ * {@link MapperFeature#AUTO_DETECT_GETTERS}/{@link MapperFeature#AUTO_DETECT_IS_GETTERS}
+ * options.
*/
public void setAutoDetectGettersSetters(boolean autoDetectGettersSetters) {
this.builder.autoDetectGettersSetters(autoDetectGettersSetters);
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 0986bafa..e2ef12b9 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
@@ -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,17 +24,18 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
/**
- * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that
- * can read and write JSON using <a href="http://jackson.codehaus.org/">Jackson 2.x's</a> {@link ObjectMapper}.
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} that can read and
+ * write JSON using <a href="http://wiki.fasterxml.com/JacksonHome">Jackson 2.x's</a> {@link ObjectMapper}.
*
- * <p>This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances.
+ * <p>This converter can be used to bind to typed beans, or untyped {@code HashMap} instances.
*
- * <p>By default, this converter supports {@code application/json} and {@code application/*+json}.
- * This can be overridden by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
+ * <p>By default, this converter supports {@code application/json} and {@code application/*+json}
+ * with {@code UTF-8} character set. This can be overridden by setting the
+ * {@link #setSupportedMediaTypes supportedMediaTypes} property.
*
* <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}.
*
- * <p>Compatible with Jackson 2.1 and higher.
+ * <p>Compatible with Jackson 2.1 to 2.6.
*
* @author Arjen Poutsma
* @author Keith Donald
@@ -62,7 +63,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMes
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
- super(objectMapper, new MediaType("application", "json", DEFAULT_CHARSET),
+ super(objectMapper, MediaType.APPLICATION_JSON_UTF8,
new MediaType("application", "*+json", DEFAULT_CHARSET));
}
@@ -76,15 +77,14 @@ public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMes
}
/**
- * Indicate whether the JSON output by this view should be prefixed with "{} &&". Default is false.
+ * Indicate whether the JSON output by this view should be prefixed with ")]}', ". Default is false.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
* The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
- * This prefix does not affect the evaluation of JSON, but if JSON validation is performed on the
- * string, the prefix would need to be ignored.
+ * This prefix should be stripped before parsing the string as JSON.
* @see #setJsonPrefix
*/
public void setPrefixJson(boolean prefixJson) {
- this.jsonPrefix = (prefixJson ? "{} && " : null);
+ this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonInputMessage.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonInputMessage.java
new file mode 100644
index 00000000..297b942d
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonInputMessage.java
@@ -0,0 +1,70 @@
+/*
+ * 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.converter.json;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+
+/**
+ * {@link HttpInputMessage} that can eventually stores a Jackson view that will be used
+ * to deserialize the message.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public class MappingJacksonInputMessage implements HttpInputMessage {
+
+ private final InputStream body;
+
+ private final HttpHeaders headers;
+
+ private Class<?> deserializationView;
+
+
+ public MappingJacksonInputMessage(InputStream body, HttpHeaders headers) {
+ this.body = body;
+ this.headers = headers;
+ }
+
+ public MappingJacksonInputMessage(InputStream body, HttpHeaders headers, Class<?> deserializationView) {
+ this(body, headers);
+ this.deserializationView = deserializationView;
+ }
+
+
+ @Override
+ public InputStream getBody() throws IOException {
+ return this.body;
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return this.headers;
+ }
+
+ public void setDeserializationView(Class<?> deserializationView) {
+ this.deserializationView = deserializationView;
+ }
+
+ public Class<?> getDeserializationView() {
+ return this.deserializationView;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java
index 1640f394..016fe94a 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -16,6 +16,8 @@
package org.springframework.http.converter.json;
+import com.fasterxml.jackson.databind.ser.FilterProvider;
+
/**
* A simple holder for the POJO to serialize via
* {@link MappingJackson2HttpMessageConverter} along with further
@@ -37,6 +39,8 @@ public class MappingJacksonValue {
private Class<?> serializationView;
+ private FilterProvider filters;
+
private String jsonpFunction;
@@ -82,6 +86,27 @@ public class MappingJacksonValue {
}
/**
+ * Set the Jackson filter provider to serialize the POJO with.
+ * @since 4.2
+ * @see com.fasterxml.jackson.databind.ObjectMapper#writer(FilterProvider)
+ * @see com.fasterxml.jackson.annotation.JsonFilter
+ * @see Jackson2ObjectMapperBuilder#filters(FilterProvider)
+ */
+ public void setFilters(FilterProvider filters) {
+ this.filters = filters;
+ }
+
+ /**
+ * Return the Jackson filter provider to use.
+ * @since 4.2
+ * @see com.fasterxml.jackson.databind.ObjectMapper#writer(FilterProvider)
+ * @see com.fasterxml.jackson.annotation.JsonFilter
+ */
+ public FilterProvider getFilters() {
+ return this.filters;
+ }
+
+ /**
* Set the name of the JSONP function name.
*/
public void setJsonpFunction(String functionName) {
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 281d00a7..388b5533 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
@@ -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.
@@ -30,7 +30,6 @@ import com.googlecode.protobuf.format.HtmlFormat;
import com.googlecode.protobuf.format.JsonFormat;
import com.googlecode.protobuf.format.XmlFormat;
-import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
@@ -41,20 +40,20 @@ import org.springframework.util.FileCopyUtils;
/**
- * An {@code HttpMessageConverter} that can read and write Protobuf
- * {@link com.google.protobuf.Message} using
- * <a href="https://developers.google.com/protocol-buffers/">Google Protocol buffers</a>.
+ * 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>.
*
- * <p>By default it supports {@code "application/json"}, {@code "application/xml"},
- * {@code "text/plain"} and {@code "application/x-protobuf"} while writing also
- * supports {@code "text/html"}
+ * <p>By default, it supports {@code "application/x-protobuf"}, {@code "text/plain"},
+ * {@code "application/json"}, {@code "application/xml"}, while also writing {@code "text/html"}.
*
- * <p>To generate Message Java classes you need to install the protoc binary.
+ * <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary.
*
- * <p>Tested against Protobuf version 2.5.0.
+ * <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.)
*
* @author Alex Antonov
* @author Brian Clozel
+ * @author Juergen Hoeller
* @since 4.1
*/
public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
@@ -67,10 +66,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message";
- private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<Class<?>, Method>();
+ private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<Class<?>, Method>();
- private ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
+ private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
/**
@@ -85,7 +84,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
* that allows the registration of message extensions.
*/
public ProtobufHttpMessageConverter(ExtensionRegistryInitializer registryInitializer) {
- super(PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON);
+ super(PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
if (registryInitializer != null) {
registryInitializer.initializeExtensionRegistry(this.extensionRegistry);
}
@@ -98,25 +97,35 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
}
@Override
+ protected MediaType getDefaultContentType(Message message) {
+ return PROTOBUF;
+ }
+
+ @Override
protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
- contentType = (contentType != null ? contentType : PROTOBUF);
-
- Charset charset = getCharset(inputMessage.getHeaders());
- InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset);
+ if (contentType == null) {
+ contentType = PROTOBUF;
+ }
+ Charset charset = contentType.getCharSet();
+ if (charset == null) {
+ charset = DEFAULT_CHARSET;
+ }
try {
Message.Builder builder = getMessageBuilder(clazz);
-
- if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
- JsonFormat.merge(reader, this.extensionRegistry, builder);
- }
- else if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
+ if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
+ InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset);
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);
+ }
else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
+ InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset);
XmlFormat.merge(reader, this.extensionRegistry, builder);
}
else {
@@ -124,29 +133,9 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
}
return builder.build();
}
- catch (Exception e) {
- throw new HttpMessageNotReadableException("Could not read Protobuf message: " + e.getMessage(), e);
- }
- }
-
- private Charset getCharset(HttpHeaders headers) {
- if (headers == null || headers.getContentType() == null || headers.getContentType().getCharSet() == null) {
- return DEFAULT_CHARSET;
- }
- return headers.getContentType().getCharSet();
- }
-
- /**
- * Create a new {@code Message.Builder} instance for the given class.
- * <p>This method uses a ConcurrentHashMap for caching method lookups.
- */
- private Message.Builder getMessageBuilder(Class<? extends Message> clazz) throws Exception {
- Method method = methodCache.get(clazz);
- if (method == null) {
- method = clazz.getMethod("newBuilder");
- methodCache.put(clazz, method);
+ catch (Exception ex) {
+ throw new HttpMessageNotReadableException("Could not read Protobuf message: " + ex.getMessage(), ex);
}
- return (Message.Builder) method.invoke(clazz);
}
/**
@@ -155,7 +144,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
*/
@Override
protected boolean canWrite(MediaType mediaType) {
- return super.canWrite(mediaType) || MediaType.TEXT_HTML.isCompatibleWith(mediaType);
+ return (super.canWrite(mediaType) || MediaType.TEXT_HTML.isCompatibleWith(mediaType));
}
@Override
@@ -163,38 +152,40 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
- Charset charset = getCharset(contentType);
+ if (contentType == null) {
+ contentType = getDefaultContentType(message);
+ }
+ Charset charset = contentType.getCharSet();
+ if (charset == null) {
+ charset = DEFAULT_CHARSET;
+ }
- if (MediaType.TEXT_HTML.isCompatibleWith(contentType)) {
- final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
- HtmlFormat.print(message, outputStreamWriter);
+ if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
+ OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
+ TextFormat.print(message, outputStreamWriter);
outputStreamWriter.flush();
}
else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
- final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
+ OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
JsonFormat.print(message, outputStreamWriter);
outputStreamWriter.flush();
}
- else if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
- final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
- TextFormat.print(message, outputStreamWriter);
- outputStreamWriter.flush();
- }
else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
- final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
+ OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
XmlFormat.print(message, outputStreamWriter);
outputStreamWriter.flush();
}
+ else if (MediaType.TEXT_HTML.isCompatibleWith(contentType)) {
+ OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
+ HtmlFormat.print(message, outputStreamWriter);
+ outputStreamWriter.flush();
+ }
else if (PROTOBUF.isCompatibleWith(contentType)) {
setProtoHeader(outputMessage, message);
FileCopyUtils.copy(message.toByteArray(), outputMessage.getBody());
}
}
- private Charset getCharset(MediaType contentType) {
- return contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET;
- }
-
/**
* Set the "X-Protobuf-*" HTTP headers when responding with a message of
* content type "application/x-protobuf"
@@ -206,9 +197,18 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName());
}
- @Override
- protected MediaType getDefaultContentType(Message message) {
- return PROTOBUF;
+
+ /**
+ * Create a new {@code Message.Builder} instance for the given class.
+ * <p>This method uses a ConcurrentHashMap for caching method lookups.
+ */
+ private static Message.Builder getMessageBuilder(Class<? extends Message> clazz) throws Exception {
+ Method method = methodCache.get(clazz);
+ if (method == null) {
+ method = clazz.getMethod("newBuilder");
+ methodCache.put(clazz, method);
+ }
+ return (Message.Builder) method.invoke(clazz);
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
index 005b9cec..d93e6ff1 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -19,8 +19,10 @@ package org.springframework.http.converter.support;
import javax.xml.transform.Source;
import org.springframework.http.converter.FormHttpMessageConverter;
+import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
+import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.ClassUtils;
@@ -29,6 +31,7 @@ import org.springframework.util.ClassUtils;
* adding support for XML and JSON-based parts.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.2
*/
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {
@@ -40,15 +43,30 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
+ private static final boolean jackson2XmlPresent =
+ ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
+
+ private static final boolean gsonPresent =
+ ClassUtils.isPresent("com.google.gson.Gson", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
+
public AllEncompassingFormHttpMessageConverter() {
addPartConverter(new SourceHttpMessageConverter<Source>());
- if (jaxb2Present) {
+
+ if (jaxb2Present && !jackson2Present) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
+
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
}
+ else if (gsonPresent) {
+ addPartConverter(new GsonHttpMessageConverter());
+ }
+
+ if (jackson2XmlPresent) {
+ addPartConverter(new MappingJackson2XmlHttpMessageConverter());
+ }
}
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java
index a4dcd859..91bc8f27 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -16,7 +16,6 @@
package org.springframework.http.converter.xml;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -40,10 +39,13 @@ import javax.xml.transform.Source;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.util.StreamUtils;
/**
* An {@code HttpMessageConverter} that can read XML collections using JAXB2.
@@ -112,6 +114,15 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
return false;
}
+ /**
+ * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter
+ * does not convert collections to XML.
+ */
+ @Override
+ public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
+ return false;
+ }
+
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write
@@ -182,10 +193,10 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
collectionClass.getName() + "]: " + ex.getMessage());
}
}
- else if (List.class.equals(collectionClass)) {
+ else if (List.class == collectionClass) {
return (T) new ArrayList();
}
- else if (SortedSet.class.equals(collectionClass)) {
+ else if (SortedSet.class == collectionClass) {
return (T) new TreeSet();
}
else {
@@ -217,6 +228,12 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
}
@Override
+ public void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage)
+ throws IOException, HttpMessageNotWritableException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException {
throw new UnsupportedOperationException();
}
@@ -239,7 +256,7 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() {
@Override
public Object resolveEntity(String publicID, String systemID, String base, String ns) {
- return new ByteArrayInputStream(new byte[0]);
+ return StreamUtils.emptyInput();
}
};
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 2decf67b..53773a48 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
@@ -47,16 +47,22 @@ import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.ClassUtils;
/**
- * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}
- * that can read and write XML using JAXB2.
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter
+ * 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}, or subclasses thereof.
+ * <p>This converter can read classes annotated with {@link XmlRootElement} and
+ * {@link XmlType}, and write classes annotated with with {@link XmlRootElement},
+ * or subclasses thereof.
+ *
+ * <p>Note that if using Spring's Marshaller/Unmarshaller abstractions from the
+ * {@code spring-oxm} module you should can the
+ * {@link MarshallingHttpMessageConverter} instead.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 3.0
+ * @see MarshallingHttpMessageConverter
*/
public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> {
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 1a2d80a2..03084759 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
@@ -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.
@@ -29,12 +29,13 @@ import org.springframework.util.Assert;
* that can read and write XML using <a href="https://github.com/FasterXML/jackson-dataformat-xml">
* Jackson 2.x extension component for reading and writing XML encoded data</a>.
*
- * <p>By default, this converter supports {@code application/xml}, {@code text/xml}, and {@code application/*+xml}.
- * This can be overridden by setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property.
+ * <p>By default, this converter supports {@code application/xml}, {@code text/xml}, and
+ * {@code application/*+xml} with {@code UTF-8} character set. This can be overridden by
+ * setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
*
* <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}.
*
- * <p>Compatible with Jackson 2.1 and higher.
+ * <p>Compatible with Jackson 2.1 to 2.6.
*
* @author Sebastien Deleuze
* @since 4.1
@@ -71,4 +72,5 @@ public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2Http
Assert.isAssignable(XmlMapper.class, objectMapper.getClass());
super.setObjectMapper(objectMapper);
}
+
}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java
index c0c1daa2..797732ef 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.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.
@@ -55,7 +55,7 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve
/**
* Construct a new {@code MarshallingHttpMessageConverter} with no {@link Marshaller} or
* {@link Unmarshaller} set. The Marshaller and Unmarshaller must be set after construction
- * by invoking {@link #setMarshaller(Marshaller)} and {@link #setUnmarshaller(Unmarshaller)} .
+ * by invoking {@link #setMarshaller(Marshaller)} and {@link #setUnmarshaller(Unmarshaller)}.
*/
public MarshallingHttpMessageConverter() {
}
@@ -104,14 +104,15 @@ public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConve
this.unmarshaller = unmarshaller;
}
+
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
- return canRead(mediaType) && (this.unmarshaller != null) && this.unmarshaller.supports(clazz);
+ return (canRead(mediaType) && this.unmarshaller != null && this.unmarshaller.supports(clazz));
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
- return canWrite(mediaType) && (this.marshaller != null) && this.marshaller.supports(clazz);
+ return (canWrite(mediaType) && this.marshaller != null && this.marshaller.supports(clazz));
}
@Override
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 0525c535..e9875107 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
@@ -140,16 +140,16 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
throws IOException, HttpMessageNotReadableException {
InputStream body = inputMessage.getBody();
- if (DOMSource.class.equals(clazz)) {
+ if (DOMSource.class == clazz) {
return (T) readDOMSource(body);
}
- else if (SAXSource.class.equals(clazz)) {
+ else if (SAXSource.class == clazz) {
return (T) readSAXSource(body);
}
- else if (StAXSource.class.equals(clazz)) {
+ else if (StAXSource.class == clazz) {
return (T) readStAXSource(body);
}
- else if (StreamSource.class.equals(clazz) || Source.class.equals(clazz)) {
+ else if (StreamSource.class == clazz || Source.class == clazz) {
return (T) readStreamSource(body);
}
else {
@@ -289,7 +289,7 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() {
@Override
public Object resolveEntity(String publicID, String systemID, String base, String ns) {
- return new ByteArrayInputStream(new byte[0]);
+ return StreamUtils.emptyInput();
}
};
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 4a5125a8..6fda91d2 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
@@ -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.
@@ -55,8 +55,6 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
protected static final String FORM_CHARSET = "UTF-8";
- private static final String METHOD_POST = "POST";
-
private final HttpServletRequest servletRequest;
@@ -84,15 +82,18 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
@Override
public HttpMethod getMethod() {
- return HttpMethod.valueOf(this.servletRequest.getMethod());
+ return HttpMethod.resolve(this.servletRequest.getMethod());
}
@Override
public URI getURI() {
try {
- return new URI(this.servletRequest.getScheme(), null, this.servletRequest.getServerName(),
- this.servletRequest.getServerPort(), this.servletRequest.getRequestURI(),
- this.servletRequest.getQueryString(), null);
+ StringBuffer url = this.servletRequest.getRequestURL();
+ String query = this.servletRequest.getQueryString();
+ if (StringUtils.hasText(query)) {
+ url.append('?').append(query);
+ }
+ return new URI(url.toString());
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex);
@@ -131,7 +132,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
this.headers.setContentType(newContentType);
}
}
- if (this.headers.getContentLength() == -1) {
+ if (this.headers.getContentLength() < 0) {
int requestContentLength = this.servletRequest.getContentLength();
if (requestContentLength != -1) {
this.headers.setContentLength(requestContentLength);
@@ -180,7 +181,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
private static boolean isFormPost(HttpServletRequest request) {
String contentType = request.getContentType();
return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
- METHOD_POST.equalsIgnoreCase(request.getMethod()));
+ HttpMethod.POST.matches(request.getMethod()));
}
/**
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 88ab968c..1aebe518 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
@@ -50,6 +50,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
private boolean headersWritten = false;
+ private boolean bodyUsed = false;
+
/**
* Construct a new instance of the ServletServerHttpResponse based on the given {@link HttpServletResponse}.
@@ -81,6 +83,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public OutputStream getBody() throws IOException {
+ this.bodyUsed = true;
writeHeaders();
return this.servletResponse.getOutputStream();
}
@@ -88,7 +91,9 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public void flush() throws IOException {
writeHeaders();
- this.servletResponse.flushBuffer();
+ if (this.bodyUsed) {
+ this.servletResponse.flushBuffer();
+ }
}
@Override
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 68202e92..7b0924ba 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-2014 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.
@@ -28,6 +28,7 @@ import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.Configurable;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
@@ -72,6 +73,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
private RequestConfig requestConfig;
+
/**
* Create a new instance of the HttpComponentsHttpInvokerRequestExecutor with a default
* {@link HttpClient} that uses a default {@code org.apache.http.impl.conn.PoolingClientConnectionManager}.
@@ -81,20 +83,6 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
.setSocketTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS).build());
}
- private static HttpClient createDefaultHttpClient() {
- Registry<ConnectionSocketFactory> schemeRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
- .register("http", PlainConnectionSocketFactory.getSocketFactory())
- .register("https", SSLConnectionSocketFactory.getSocketFactory())
- .build();
-
- PoolingHttpClientConnectionManager connectionManager
- = new PoolingHttpClientConnectionManager(schemeRegistry);
- connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS);
- connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
-
- return HttpClientBuilder.create().setConnectionManager(connectionManager).build();
- }
-
/**
* Create a new instance of the HttpComponentsClientHttpRequestFactory
* with the given {@link HttpClient} instance.
@@ -109,6 +97,21 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
this.requestConfig = requestConfig;
}
+
+ private static HttpClient createDefaultHttpClient() {
+ Registry<ConnectionSocketFactory> schemeRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", SSLConnectionSocketFactory.getSocketFactory())
+ .build();
+
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(schemeRegistry);
+ connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS);
+ connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
+
+ return HttpClientBuilder.create().setConnectionManager(connectionManager).build();
+ }
+
+
/**
* Set the {@link HttpClient} instance to use for this request executor.
*/
@@ -133,8 +136,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
*/
public void setConnectTimeout(int timeout) {
Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
- this.requestConfig = cloneRequestConfig()
- .setConnectTimeout(timeout).build();
+ this.requestConfig = cloneRequestConfig().setConnectTimeout(timeout).build();
setLegacyConnectionTimeout(getHttpClient(), timeout);
}
@@ -155,8 +157,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)) {
- client.getParams().setIntParameter(
- org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
+ client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
}
}
@@ -170,8 +171,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
* @see RequestConfig#getConnectionRequestTimeout()
*/
public void setConnectionRequestTimeout(int connectionRequestTimeout) {
- this.requestConfig = cloneRequestConfig()
- .setConnectionRequestTimeout(connectionRequestTimeout).build();
+ this.requestConfig = cloneRequestConfig().setConnectionRequestTimeout(connectionRequestTimeout).build();
}
/**
@@ -185,8 +185,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
*/
public void setReadTimeout(int timeout) {
Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
- this.requestConfig = cloneRequestConfig()
- .setSocketTimeout(timeout).build();
+ this.requestConfig = cloneRequestConfig().setSocketTimeout(timeout).build();
setLegacySocketTimeout(getHttpClient(), timeout);
}
@@ -200,15 +199,15 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
@SuppressWarnings("deprecation")
private void setLegacySocketTimeout(HttpClient client, int timeout) {
if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) {
- client.getParams().setIntParameter(
- org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout);
+ client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout);
}
}
private RequestConfig.Builder cloneRequestConfig() {
- return this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom();
+ return (this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom());
}
+
/**
* Execute the given request through the HttpClient.
* <p>This method implements the basic processing workflow:
@@ -269,12 +268,39 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke
* Create a {@link RequestConfig} for the given configuration. Can return {@code null}
* to indicate that no custom request config should be set and the defaults of the
* {@link HttpClient} should be used.
+ * <p>The default implementation tries to merge the defaults of the client with the
+ * local customizations of the instance, if any.
* @param config the HTTP invoker configuration that specifies the
* target service
* @return the RequestConfig to use
*/
protected RequestConfig createRequestConfig(HttpInvokerClientConfiguration config) {
- return (this.requestConfig != null ? this.requestConfig : null);
+ HttpClient client = getHttpClient();
+ if (client instanceof Configurable) {
+ RequestConfig clientRequestConfig = ((Configurable) client).getConfig();
+ return mergeRequestConfig(clientRequestConfig);
+ }
+ return this.requestConfig;
+ }
+
+ private RequestConfig mergeRequestConfig(RequestConfig defaultRequestConfig) {
+ if (this.requestConfig == null) { // nothing to merge
+ return defaultRequestConfig;
+ }
+ RequestConfig.Builder builder = RequestConfig.copy(defaultRequestConfig);
+ int connectTimeout = this.requestConfig.getConnectTimeout();
+ if (connectTimeout >= 0) {
+ builder.setConnectTimeout(connectTimeout);
+ }
+ int connectionRequestTimeout = this.requestConfig.getConnectionRequestTimeout();
+ if (connectionRequestTimeout >= 0) {
+ builder.setConnectionRequestTimeout(connectionRequestTimeout);
+ }
+ int socketTimeout = this.requestConfig.getSocketTimeout();
+ if (socketTimeout >= 0) {
+ builder.setSocketTimeout(socketTimeout);
+ }
+ return builder.build();
}
/**
diff --git a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean.java b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean.java
index 3514f5e2..1386ea1a 100644
--- a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean.java
+++ b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerProxyFactoryBean.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.
@@ -36,6 +36,11 @@ import org.springframework.beans.factory.FactoryBean;
* expense of being tied to Java. Nevertheless, it is as easy to set up as
* Hessian and Burlap, which is its main advantage compared to RMI.
*
+ * <p><b>WARNING: Be aware of vulnerabilities due to unsafe Java deserialization:
+ * Manipulated input streams could lead to unwanted code execution on the server
+ * during the deserialization step. As a consequence, do not expose HTTP invoker
+ * endpoints to untrusted clients but rather just between your own services.</b>
+ *
* @author Juergen Hoeller
* @since 1.1
* @see #setServiceInterface
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 0e85a7bc..bcbc09f9 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
@@ -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.
@@ -47,6 +47,11 @@ import org.springframework.web.util.NestedServletException;
* expense of being tied to Java. Nevertheless, it is as easy to set up as
* Hessian and Burlap, which is its main advantage compared to RMI.
*
+ * <p><b>WARNING: Be aware of vulnerabilities due to unsafe Java deserialization:
+ * Manipulated input streams could lead to unwanted code execution on the server
+ * during the deserialization step. As a consequence, do not expose HTTP invoker
+ * endpoints to untrusted clients but rather just between your own services.</b>
+ *
* @author Juergen Hoeller
* @since 1.1
* @see HttpInvokerClientInterceptor
diff --git a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java
index 44096bb5..13caeda9 100644
--- a/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java
+++ b/spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -17,8 +17,9 @@
package org.springframework.web;
import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashSet;
+import java.util.EnumSet;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Set;
import javax.servlet.ServletException;
@@ -105,11 +106,14 @@ public class HttpRequestMethodNotSupportedException extends ServletException {
* Return the actually supported HTTP methods, if known, as {@link HttpMethod} instances.
*/
public Set<HttpMethod> getSupportedHttpMethods() {
- Set<HttpMethod> supportedMethods = new LinkedHashSet<HttpMethod>();
+ List<HttpMethod> supportedMethods = new LinkedList<HttpMethod>();
for (String value : this.supportedMethods) {
- supportedMethods.add(HttpMethod.valueOf(value));
+ HttpMethod resolved = HttpMethod.resolve(value);
+ if (resolved != null) {
+ supportedMethods.add(resolved);
+ }
}
- return Collections.unmodifiableSet(supportedMethods);
+ return EnumSet.copyOf(supportedMethods);
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/SpringServletContainerInitializer.java b/spring-web/src/main/java/org/springframework/web/SpringServletContainerInitializer.java
index 7b412426..b5a43b7b 100644
--- a/spring-web/src/main/java/org/springframework/web/SpringServletContainerInitializer.java
+++ b/spring-web/src/main/java/org/springframework/web/SpringServletContainerInitializer.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.
@@ -113,18 +113,14 @@ public class SpringServletContainerInitializer implements ServletContainerInitia
/**
* Delegate the {@code ServletContext} to any {@link WebApplicationInitializer}
* implementations present on the application classpath.
- *
* <p>Because this class declares @{@code HandlesTypes(WebApplicationInitializer.class)},
* Servlet 3.0+ containers will automatically scan the classpath for implementations
* of Spring's {@code WebApplicationInitializer} interface and provide the set of all
* such types to the {@code webAppInitializerClasses} parameter of this method.
- *
- * <p>If no {@code WebApplicationInitializer} implementations are found on the
- * classpath, this method is effectively a no-op. An INFO-level log message will be
- * issued notifying the user that the {@code ServletContainerInitializer} has indeed
- * been invoked but that no {@code WebApplicationInitializer} implementations were
- * found.
- *
+ * <p>If no {@code WebApplicationInitializer} implementations are found on the classpath,
+ * this method is effectively a no-op. An INFO-level log message will be issued notifying
+ * the user that the {@code ServletContainerInitializer} has indeed been invoked but that
+ * no {@code WebApplicationInitializer} implementations were found.
* <p>Assuming that one or more {@code WebApplicationInitializer} types are detected,
* they will be instantiated (and <em>sorted</em> if the @{@link
* org.springframework.core.annotation.Order @Order} annotation is present or
@@ -134,7 +130,6 @@ public class SpringServletContainerInitializer implements ServletContainerInitia
* that each instance may register and configure servlets such as Spring's
* {@code DispatcherServlet}, listeners such as Spring's {@code ContextLoaderListener},
* or any other Servlet API componentry such as filters.
- *
* @param webAppInitializerClasses all implementations of
* {@link WebApplicationInitializer} found on the application classpath
* @param servletContext the servlet context to be initialized
@@ -168,9 +163,8 @@ public class SpringServletContainerInitializer implements ServletContainerInitia
return;
}
+ servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
- servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);
-
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
diff --git a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java
index c4524cae..0568b904 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -26,19 +26,30 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * A base class for ContentNegotiationStrategy types that maintain a map with keys
- * such as "json" and media types such as "application/json".
+ * Base class for {@code ContentNegotiationStrategy} implementations with the
+ * steps to resolve a request to media types.
+ *
+ * <p>First a key (e.g. "json", "pdf") must be extracted from the request (e.g.
+ * file extension, query param). The key must then be resolved to media type(s)
+ * through the base class {@link MappingMediaTypeFileExtensionResolver} which
+ * stores such mappings.
+ *
+ * <p>The method {@link #handleNoMatch} allow sub-classes to plug in additional
+ * ways of looking up media types (e.g. through the Java Activation framework,
+ * or {@link javax.servlet.ServletContext#getMimeType}. Media types resolved
+ * via base classes are then added to the base class
+ * {@link MappingMediaTypeFileExtensionResolver}, i.e. cached for new lookups.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
-public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver
- implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
+public abstract class AbstractMappingContentNegotiationStrategy
+ extends MappingMediaTypeFileExtensionResolver
+ implements ContentNegotiationStrategy {
/**
- * Create an instance with the given extension-to-MediaType lookup.
- * @throws IllegalArgumentException if a media type string cannot be parsed
+ * Create an instance with the given map of file extensions and media types.
*/
public AbstractMappingContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes);
@@ -46,7 +57,9 @@ public abstract class AbstractMappingContentNegotiationStrategy extends MappingM
@Override
- public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
+ public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
+ throws HttpMediaTypeNotAcceptableException {
+
return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
}
@@ -74,22 +87,27 @@ public abstract class AbstractMappingContentNegotiationStrategy extends MappingM
}
/**
- * Sub-classes must extract the key to use to look up a media type.
- * @return the lookup key or {@code null} if the key cannot be derived
+ * Extract a key from the request to use to look up media types.
+ * @return the lookup key or {@code null}.
*/
protected abstract String getMediaTypeKey(NativeWebRequest request);
/**
- * Invoked when a matching media type is found in the lookup map.
+ * Override to provide handling when a key is successfully resolved via
+ * {@link #lookupMediaType}.
*/
- protected void handleMatch(String mappingKey, MediaType mediaType) {
+ protected void handleMatch(String key, MediaType mediaType) {
}
/**
- * Invoked when no matching media type is found in the lookup map.
- * Sub-classes can take further steps to determine the media type.
+ * Override to provide handling when a key is not resolved via.
+ * {@link #lookupMediaType}. Sub-classes can take further steps to
+ * determine the media type(s). If a MediaType is returned from
+ * this method it will be added to the cache in the base class.
*/
- protected MediaType handleNoMatch(NativeWebRequest request, String key) throws HttpMediaTypeNotAcceptableException {
+ protected MediaType handleNoMatch(NativeWebRequest request, String key)
+ throws HttpMediaTypeNotAcceptableException {
+
return null;
}
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 8dd94eba..4450ad10 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -30,63 +30,52 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * This class is used to determine the requested {@linkplain MediaType media types}
- * of a request by delegating to a list of ContentNegotiationStrategy instances.
- * The strategies must be provided at instantiation or alternatively if using
- * the default constructor, an instance of {@link HeaderContentNegotiationStrategy}
- * will be configured by default.
+ * Central class to determine requested {@linkplain MediaType media types}
+ * for a request. This is done by delegating to a list of configured
+ * {@code ContentNegotiationStrategy} instances.
*
- * <p>This class may also be used to look up file extensions associated with a
- * MediaType. This is done by consulting the list of configured
- * {@link MediaTypeFileExtensionResolver} instances. Note that some
- * ContentNegotiationStrategy implementations also implement
- * MediaTypeFileExtensionResolver and the class constructor accepting the former
- * will also detect if they implement the latter. If you need to register additional
- * resolvers, you can use the method
- * {@link #addFileExtensionResolvers(MediaTypeFileExtensionResolver...)}.
+ * <p>Also provides methods to look up file extensions for a media type.
+ * This is done by delegating to the list of configured
+ * {@code MediaTypeFileExtensionResolver} instances.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
+public class ContentNegotiationManager implements ContentNegotiationStrategy,
+ MediaTypeFileExtensionResolver {
- private static final List<MediaType> MEDIA_TYPE_ALL = Arrays.asList(MediaType.ALL);
+ private static final List<MediaType> MEDIA_TYPE_ALL =
+ Collections.<MediaType>singletonList(MediaType.ALL);
- private final List<ContentNegotiationStrategy> contentNegotiationStrategies =
+
+ private final List<ContentNegotiationStrategy> strategies =
new ArrayList<ContentNegotiationStrategy>();
- private final Set<MediaTypeFileExtensionResolver> fileExtensionResolvers =
+ private final Set<MediaTypeFileExtensionResolver> resolvers =
new LinkedHashSet<MediaTypeFileExtensionResolver>();
/**
- * Create an instance with the given ContentNegotiationStrategy instances.
- * <p>Each instance is checked to see if it is also an implementation of
- * MediaTypeFileExtensionResolver, and if so it is registered as such.
- * @param strategies one more more ContentNegotiationStrategy instances
+ * Create an instance with the given list of
+ * {@code ContentNegotiationStrategy} strategies each of which may also be
+ * an instance of {@code MediaTypeFileExtensionResolver}.
+ * @param strategies the strategies to use
*/
public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
- Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected");
- this.contentNegotiationStrategies.addAll(Arrays.asList(strategies));
- for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
- if (strategy instanceof MediaTypeFileExtensionResolver) {
- this.fileExtensionResolvers.add((MediaTypeFileExtensionResolver) strategy);
- }
- }
+ this(Arrays.asList(strategies));
}
/**
- * Create an instance with the given ContentNegotiationStrategy instances.
- * <p>Each instance is checked to see if it is also an implementation of
- * MediaTypeFileExtensionResolver, and if so it is registered as such.
- * @param strategies one more more ContentNegotiationStrategy instances
+ * A collection-based alternative to
+ * {@link #ContentNegotiationManager(ContentNegotiationStrategy...)}.
+ * @param strategies the strategies to use
*/
public ContentNegotiationManager(Collection<ContentNegotiationStrategy> strategies) {
Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected");
- this.contentNegotiationStrategies.addAll(strategies);
- for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
+ this.strategies.addAll(strategies);
+ for (ContentNegotiationStrategy strategy : this.strategies) {
if (strategy instanceof MediaTypeFileExtensionResolver) {
- this.fileExtensionResolvers.add((MediaTypeFileExtensionResolver) strategy);
+ this.resolvers.add((MediaTypeFileExtensionResolver) strategy);
}
}
}
@@ -104,32 +93,24 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
* @since 3.2.16
*/
public List<ContentNegotiationStrategy> getStrategies() {
- return this.contentNegotiationStrategies;
+ return this.strategies;
}
/**
- * Add MediaTypeFileExtensionResolver instances.
- * <p>Note that some {@link ContentNegotiationStrategy} implementations also
- * implement {@link MediaTypeFileExtensionResolver} and the class constructor
- * accepting the former will also detect implementations of the latter. Therefore
- * you only need to use this method to register additional instances.
- * @param resolvers one or more resolvers
+ * Register more {@code MediaTypeFileExtensionResolver} instances in addition
+ * to those detected at construction.
+ * @param resolvers the resolvers to add
*/
public void addFileExtensionResolvers(MediaTypeFileExtensionResolver... resolvers) {
- this.fileExtensionResolvers.addAll(Arrays.asList(resolvers));
+ this.resolvers.addAll(Arrays.asList(resolvers));
}
- /**
- * Delegate to all configured ContentNegotiationStrategy instances until one
- * returns a non-empty list.
- * @param webRequest the current request
- * @return the requested media types or an empty list, never {@code null}
- * @throws HttpMediaTypeNotAcceptableException if the requested media types cannot be parsed
- */
@Override
- public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
- for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
- List<MediaType> mediaTypes = strategy.resolveMediaTypes(webRequest);
+ public List<MediaType> resolveMediaTypes(NativeWebRequest request)
+ throws HttpMediaTypeNotAcceptableException {
+
+ for (ContentNegotiationStrategy strategy : this.strategies) {
+ List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
continue;
}
@@ -138,27 +119,29 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
return Collections.emptyList();
}
- /**
- * Delegate to all configured MediaTypeFileExtensionResolver instances and aggregate
- * the list of all file extensions found.
- */
@Override
public List<String> resolveFileExtensions(MediaType mediaType) {
Set<String> result = new LinkedHashSet<String>();
- for (MediaTypeFileExtensionResolver resolver : this.fileExtensionResolvers) {
+ for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
result.addAll(resolver.resolveFileExtensions(mediaType));
}
return new ArrayList<String>(result);
}
/**
- * Delegate to all configured MediaTypeFileExtensionResolver instances and aggregate
- * the list of all known file extensions.
+ * {@inheritDoc}
+ * <p>At startup this method returns extensions explicitly registered with
+ * either {@link PathExtensionContentNegotiationStrategy} or
+ * {@link ParameterContentNegotiationStrategy}. At runtime if there is a
+ * "path extension" strategy and its
+ * {@link PathExtensionContentNegotiationStrategy#setUseJaf(boolean)
+ * useJaf} property is set to "true", the list of extensions may
+ * increase as file extensions are resolved via JAF and cached.
*/
@Override
public List<String> getAllFileExtensions() {
Set<String> result = new LinkedHashSet<String>();
- for (MediaTypeFileExtensionResolver resolver : this.fileExtensionResolvers) {
+ for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
result.addAll(resolver.getAllFileExtensions());
}
return new ArrayList<String>(result);
diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java
index 5b5de32c..b035b079 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java
@@ -33,13 +33,56 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.context.ServletContextAware;
/**
- * A factory providing convenient access to a {@code ContentNegotiationManager}
- * configured with one or more {@link ContentNegotiationStrategy} instances.
+ * Factory to create a {@code ContentNegotiationManager} and configure it with
+ * one or more {@link ContentNegotiationStrategy} instances via simple setters.
+ * The following table shows setters, resulting strategy instances, and if in
+ * use by default:
*
- * <p>By default strategies for checking the extension of the request path and
- * the {@code Accept} header are registered. The path extension check will perform
- * lookups through the {@link ServletContext} and the Java Activation Framework
- * (if present) unless {@linkplain #setMediaTypes media types} are configured.
+ * <table>
+ * <tr>
+ * <th>Property Setter</th>
+ * <th>Underlying Strategy</th>
+ * <th>Default Setting</th>
+ * </tr>
+ * <tr>
+ * <td>{@link #setFavorPathExtension}</td>
+ * <td>{@link PathExtensionContentNegotiationStrategy Path Extension strategy}</td>
+ * <td>On</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #setFavorParameter favorParameter}</td>
+ * <td>{@link ParameterContentNegotiationStrategy Parameter strategy}</td>
+ * <td>Off</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #setIgnoreAcceptHeader ignoreAcceptHeader}</td>
+ * <td>{@link HeaderContentNegotiationStrategy Header strategy}</td>
+ * <td>On</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #setDefaultContentType defaultContentType}</td>
+ * <td>{@link FixedContentNegotiationStrategy Fixed content strategy}</td>
+ * <td>Not set</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #setDefaultContentTypeStrategy defaultContentTypeStrategy}</td>
+ * <td>{@link ContentNegotiationStrategy}</td>
+ * <td>Not set</td>
+ * </tr>
+ * </table>
+ *
+ * <p>The order in which strategies are configured is fixed. Setters may only
+ * turn individual strategies on or off. If you need a custom order for any
+ * reason simply instantiate {@code ContentNegotiationManager} directly.
+ *
+ * <p>For the path extension and parameter strategies you may explicitly add
+ * {@link #setMediaTypes MediaType mappings}. This will be used to resolve path
+ * extensions or a parameter value such as "json" to a media type such as
+ * "application/json".
+ *
+ * <p>The path extension strategy will also use {@link ServletContext#getMimeType}
+ * and the Java Activation framework (JAF), if available, to resolve a path
+ * extension to a MediaType. You may {@link #setUseJaf suppress} the use of JAF.
*
* @author Rossen Stoyanchev
* @since 3.2
@@ -69,11 +112,11 @@ public class ContentNegotiationManagerFactoryBean
/**
- * Indicate whether the extension of the request path should be used to determine
- * the requested media type with the <em>highest priority</em>.
- * <p>By default this value is set to {@code true} in which case a request
+ * Whether the path extension in the URL path should be used to determine
+ * the requested media type.
+ * <p>By default this is set to {@code true} in which case a request
* for {@code /hotels.pdf} will be interpreted as a request for
- * {@code "application/pdf"} regardless of the {@code Accept} header.
+ * {@code "application/pdf"} regardless of the 'Accept' header.
*/
public void setFavorPathExtension(boolean favorPathExtension) {
this.favorPathExtension = favorPathExtension;
@@ -97,26 +140,25 @@ public class ContentNegotiationManagerFactoryBean
if (!CollectionUtils.isEmpty(mediaTypes)) {
for (Entry<Object, Object> entry : mediaTypes.entrySet()) {
String extension = ((String)entry.getKey()).toLowerCase(Locale.ENGLISH);
- this.mediaTypes.put(extension, MediaType.valueOf((String) entry.getValue()));
+ MediaType mediaType = MediaType.valueOf((String) entry.getValue());
+ this.mediaTypes.put(extension, mediaType);
}
}
}
/**
- * Add a mapping from a file extension to a media type.
- * <p>If no mapping is added or when an extension is not found, the Java
- * Action Framework, if available, may be used if enabled via
- * {@link #setFavorPathExtension(boolean)}.
+ * An alternative to {@link #setMediaTypes} for use in Java code.
+ * @see #setMediaTypes
+ * @see #addMediaTypes
*/
public void addMediaType(String fileExtension, MediaType mediaType) {
this.mediaTypes.put(fileExtension, mediaType);
}
/**
- * Add mappings from file extensions to media types.
- * <p>If no mappings are added or when an extension is not found, the Java
- * Action Framework, if available, may be used if enabled via
- * {@link #setFavorPathExtension(boolean)}.
+ * An alternative to {@link #setMediaTypes} for use in Java code.
+ * @see #setMediaTypes
+ * @see #addMediaType
*/
public void addMediaTypes(Map<String, MediaType> mediaTypes) {
if (mediaTypes != null) {
@@ -125,23 +167,21 @@ public class ContentNegotiationManagerFactoryBean
}
/**
- * Whether to ignore requests that have a file extension that does not match
- * any mapped media types. Setting this to {@code false} will result in a
- * {@code HttpMediaTypeNotAcceptableException} when there is no match.
- *
+ * Whether to ignore requests with path extension that cannot be resolved
+ * to any media type. Setting this to {@code false} will result in an
+ * {@code HttpMediaTypeNotAcceptableException} if there is no match.
* <p>By default this is set to {@code true}.
*/
- public void setIgnoreUnknownPathExtensions(boolean ignoreUnknownPathExtensions) {
- this.ignoreUnknownPathExtensions = ignoreUnknownPathExtensions;
+ public void setIgnoreUnknownPathExtensions(boolean ignore) {
+ this.ignoreUnknownPathExtensions = ignore;
}
/**
- * Indicate whether to use the Java Activation Framework as a fallback option
- * to map from file extensions to media types. This is used only when
- * {@link #setFavorPathExtension(boolean)} is set to {@code true}.
- * <p>The default value is {@code true}.
- * @see #setParameterName
- * @see #setMediaTypes
+ * When {@link #setFavorPathExtension favorPathExtension} is set, this
+ * property determines whether to allow use of JAF (Java Activation Framework)
+ * to resolve a path extension to a specific MediaType.
+ * <p>By default this is not set in which case
+ * {@code PathExtensionContentNegotiationStrategy} will use JAF if available.
*/
public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
@@ -152,14 +192,10 @@ public class ContentNegotiationManagerFactoryBean
}
/**
- * Indicate whether a request parameter should be used to determine the
- * requested media type with the <em>2nd highest priority</em>, i.e.
- * after path extensions but before the {@code Accept} header.
- * <p>The default value is {@code false}. If set to to {@code true}, a request
- * for {@code /hotels?format=pdf} will be interpreted as a request for
- * {@code "application/pdf"} regardless of the {@code Accept} header.
- * <p>To use this option effectively you must also configure the MediaType
- * type mappings via {@link #setMediaTypes(Properties)}.
+ * Whether a request parameter ("format" by default) should be used to
+ * determine the requested media type. For this option to work you must
+ * register {@link #setMediaTypes media type mappings}.
+ * <p>By default this is set to {@code false}.
* @see #setParameterName
*/
public void setFavorParameter(boolean favorParameter) {
@@ -167,8 +203,7 @@ public class ContentNegotiationManagerFactoryBean
}
/**
- * Set the parameter name that can be used to determine the requested media type
- * if the {@link #setFavorParameter} property is {@code true}.
+ * Set the query parameter name to use when {@link #setFavorParameter} is on.
* <p>The default parameter name is {@code "format"}.
*/
public void setParameterName(String parameterName) {
@@ -177,10 +212,7 @@ public class ContentNegotiationManagerFactoryBean
}
/**
- * Indicate whether the HTTP {@code Accept} header should be ignored altogether.
- * If set the {@code Accept} header is checked at the
- * <em>3rd highest priority</em>, i.e. after the request path extension and
- * possibly a request parameter if configured.
+ * Whether to disable checking the 'Accept' request header.
* <p>By default this value is set to {@code false}.
*/
public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
@@ -188,27 +220,28 @@ public class ContentNegotiationManagerFactoryBean
}
/**
- * Set the default content type to use when no content type was requested.
- * <p>Note that internally this method creates and adds a
- * {@link org.springframework.web.accept.FixedContentNegotiationStrategy
- * FixedContentNegotiationStrategy}. Alternatively you can also provide a
- * custom strategy via {@link #setDefaultContentTypeStrategy}.
+ * Set the default content type to use when no content type is requested.
+ * <p>By default this is not set.
+ * @see #setDefaultContentTypeStrategy
*/
- public void setDefaultContentType(MediaType defaultContentType) {
- this.defaultNegotiationStrategy = new FixedContentNegotiationStrategy(defaultContentType);
+ public void setDefaultContentType(MediaType contentType) {
+ this.defaultNegotiationStrategy = new FixedContentNegotiationStrategy(contentType);
}
/**
- * Configure a custom {@link ContentNegotiationStrategy} to use to determine
- * the default content type to use when no content type was requested.
- * <p>However also consider using {@link #setDefaultContentType} which
- * provides a simpler alternative to doing the same.
+ * Set a custom {@link ContentNegotiationStrategy} to use to determine
+ * the content type to use when no content type is requested.
+ * <p>By default this is not set.
+ * @see #setDefaultContentType
* @since 4.1.2
*/
- public void setDefaultContentTypeStrategy(ContentNegotiationStrategy defaultStrategy) {
- this.defaultNegotiationStrategy = defaultStrategy;
+ public void setDefaultContentTypeStrategy(ContentNegotiationStrategy strategy) {
+ this.defaultNegotiationStrategy = strategy;
}
+ /**
+ * Invoked by Spring to inject the ServletContext.
+ */
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
@@ -222,7 +255,8 @@ public class ContentNegotiationManagerFactoryBean
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
if (this.servletContext != null && !isUseJafTurnedOff()) {
- strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
+ strategy = new ServletPathExtensionContentNegotiationStrategy(
+ this.servletContext, this.mediaTypes);
}
else {
strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
@@ -235,7 +269,8 @@ public class ContentNegotiationManagerFactoryBean
}
if (this.favorParameter) {
- ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
+ ParameterContentNegotiationStrategy strategy =
+ new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName);
strategies.add(strategy);
}
@@ -245,13 +280,12 @@ public class ContentNegotiationManagerFactoryBean
}
if (this.defaultNegotiationStrategy != null) {
- strategies.add(defaultNegotiationStrategy);
+ strategies.add(this.defaultNegotiationStrategy);
}
this.contentNegotiationManager = new ContentNegotiationManager(strategies);
}
-
@Override
public ContentNegotiationManager getObject() {
return this.contentNegotiationManager;
diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java
index 1b1c4b9c..127f90ce 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.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.
@@ -23,7 +23,7 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * A strategy for resolving the requested media types in a request.
+ * A strategy for resolving the requested media types for a request.
*
* @author Rossen Stoyanchev
* @since 3.2
@@ -37,8 +37,10 @@ public interface ContentNegotiationStrategy {
* @param webRequest the current request
* @return the requested media types or an empty list, never {@code null}
*
- * @throws HttpMediaTypeNotAcceptableException if the requested media types cannot be parsed
+ * @throws HttpMediaTypeNotAcceptableException if the requested media
+ * types cannot be parsed
*/
- List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException;
+ List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
+ throws HttpMediaTypeNotAcceptableException;
}
diff --git a/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java
index f43668c5..547ccef7 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.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.
@@ -26,30 +26,33 @@ import org.springframework.http.MediaType;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * A ContentNegotiationStrategy that returns a fixed content type.
+ * A {@code ContentNegotiationStrategy} that returns a fixed content type.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy {
- private static final Log logger = LogFactory.getLog(FixedContentNegotiationStrategy.class);
+ private static final Log logger = LogFactory.getLog(
+ FixedContentNegotiationStrategy.class);
+
+ private final List<MediaType> contentType;
- private final MediaType defaultContentType;
/**
- * Create an instance that always returns the given content type.
+ * Create an instance with the given content type.
*/
- public FixedContentNegotiationStrategy(MediaType defaultContentType) {
- this.defaultContentType = defaultContentType;
+ public FixedContentNegotiationStrategy(MediaType contentType) {
+ this.contentType = Collections.singletonList(contentType);
}
+
@Override
- public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) {
+ public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
if (logger.isDebugEnabled()) {
- logger.debug("Requested media types is " + this.defaultContentType + " (based on default MediaType)");
+ logger.debug("Requested media types is " + this.contentType + ".");
}
- return Collections.singletonList(this.defaultContentType);
+ return this.contentType;
}
}
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 7ed82584..bcefd727 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
@@ -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.
@@ -19,6 +19,7 @@ package org.springframework.web.accept;
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;
@@ -26,34 +27,36 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * A ContentNegotiationStrategy that parses the 'Accept' header of the request.
+ * A {@code ContentNegotiationStrategy} that checks the 'Accept' request header.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
- private static final String ACCEPT_HEADER = "Accept";
/**
* {@inheritDoc}
- * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed.
+ * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header
+ * cannot be parsed.
*/
@Override
- public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
- String acceptHeader = webRequest.getHeader(ACCEPT_HEADER);
+ public List<MediaType> resolveMediaTypes(NativeWebRequest request)
+ throws HttpMediaTypeNotAcceptableException {
+
+ String header = request.getHeader(HttpHeaders.ACCEPT);
+ if (!StringUtils.hasText(header)) {
+ return Collections.emptyList();
+ }
try {
- if (StringUtils.hasText(acceptHeader)) {
- List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
- MediaType.sortBySpecificityAndQuality(mediaTypes);
- return mediaTypes;
- }
+ List<MediaType> mediaTypes = MediaType.parseMediaTypes(header);
+ MediaType.sortBySpecificityAndQuality(mediaTypes);
+ return mediaTypes;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
- "Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage());
+ "Could not parse 'Accept' header [" + header + "]: " + ex.getMessage());
}
- return Collections.emptyList();
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java
index 7acd788d..6c2c8c7d 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java
@@ -59,7 +59,9 @@ public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExten
for (Entry<String, MediaType> entries : mediaTypes.entrySet()) {
String extension = entries.getKey().toLowerCase(Locale.ENGLISH);
MediaType mediaType = entries.getValue();
- addMapping(extension, mediaType);
+ this.mediaTypes.put(extension, mediaType);
+ this.fileExtensions.add(mediaType, extension);
+ this.allFileExtensions.add(extension);
}
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/accept/MediaTypeFileExtensionResolver.java b/spring-web/src/main/java/org/springframework/web/accept/MediaTypeFileExtensionResolver.java
index 218a07fe..c063e5c5 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/MediaTypeFileExtensionResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/MediaTypeFileExtensionResolver.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.
@@ -21,8 +21,8 @@ import java.util.List;
import org.springframework.http.MediaType;
/**
- * A strategy for resolving a {@link MediaType} to one or more path extensions.
- * For example "application/json" to "json".
+ * Strategy to resolve {@link MediaType} to a list of file extensions.
+ * For example resolve "application/json" to "json".
*
* @author Rossen Stoyanchev
* @since 3.2
@@ -38,7 +38,7 @@ public interface MediaTypeFileExtensionResolver {
List<String> resolveFileExtensions(MediaType mediaType);
/**
- * Return all known file extensions.
+ * Return all registered file extensions.
* @return a list of extensions or an empty list, never {@code null}
*/
List<String> getAllFileExtensions();
diff --git a/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java
index cd706812..a2520d27 100644
--- a/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java
+++ b/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -27,51 +27,61 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * A ContentNegotiationStrategy that uses a request parameter to determine what
- * media types are requested. The default parameter name is {@code format}.
- * Its value is used to look up the media type in the map given to the constructor.
- *
+ * A {@code ContentNegotiationStrategy} that resolves a query parameter to a
+ * key to be used to look up a media type. The default parameter name is
+ * {@code format}.
+ *s
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
+public class ParameterContentNegotiationStrategy
+ extends AbstractMappingContentNegotiationStrategy {
- private static final Log logger = LogFactory.getLog(ParameterContentNegotiationStrategy.class);
+ private static final Log logger = LogFactory.getLog(
+ ParameterContentNegotiationStrategy.class);
private String parameterName = "format";
+
/**
- * Create an instance with the given extension-to-MediaType lookup.
- * @throws IllegalArgumentException if a media type string cannot be parsed
+ * Create an instance with the given map of file extensions and media types.
*/
public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes);
}
+
/**
- * Set the parameter name that can be used to determine the requested media type.
- * <p>The default parameter name is {@code format}.
+ * Set the name of the parameter to use to determine requested media types.
+ * <p>By default this is set to {@code "format"}.
*/
public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "parameterName is required");
this.parameterName = parameterName;
}
+ public String getParameterName() {
+ return this.parameterName;
+ }
+
@Override
- protected String getMediaTypeKey(NativeWebRequest webRequest) {
- return webRequest.getParameter(this.parameterName);
+ protected String getMediaTypeKey(NativeWebRequest request) {
+ return request.getParameter(getParameterName());
}
@Override
protected void handleMatch(String mediaTypeKey, MediaType mediaType) {
if (logger.isDebugEnabled()) {
- logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" +
- this.parameterName + "'='" + mediaTypeKey + "')");
+ logger.debug("Requested media type is '" + mediaType +
+ "' based on '" + getParameterName() + "'='" + mediaTypeKey + "'.");
}
}
@Override
- protected MediaType handleNoMatch(NativeWebRequest request, String key) throws HttpMediaTypeNotAcceptableException {
+ protected MediaType handleNoMatch(NativeWebRequest request, String key)
+ throws HttpMediaTypeNotAcceptableException {
+
throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
}
+
}
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 68b97e1b..9b19c271 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -38,40 +38,42 @@ import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.WebUtils;
/**
- * A ContentNegotiationStrategy that uses the path extension of the URL to
- * determine what media types are requested. The path extension is first looked
- * up in the map of media types provided to the constructor. If that fails, the
- * Java Activation framework is used as a fallback mechanism.
+ * A {@code ContentNegotiationStrategy} that resolves the file extension in the
+ * request path to a key to be used to look up a media type.
*
- * <p>
- * The presence of the Java Activation framework is detected and enabled
- * automatically but the {@link #setUseJaf(boolean)} property may be used to
- * override that setting.
+ * <p>If the file extension is not found in the explicit registrations provided
+ * to the constructor, the Java Activation Framework (JAF) is used as a fallback
+ * mechanism.
+ *
+ * <p>The presence of the JAF is detected and enabled automatically but the
+ * {@link #setUseJaf(boolean)} property may be set to false.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
-
- private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap",
- PathExtensionContentNegotiationStrategy.class.getClassLoader());
+public class PathExtensionContentNegotiationStrategy
+ extends AbstractMappingContentNegotiationStrategy {
private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class);
- private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
+ private static final boolean JAF_PRESENT = ClassUtils.isPresent(
+ "javax.activation.FileTypeMap",
+ PathExtensionContentNegotiationStrategy.class.getClassLoader());
+
+ private static final UrlPathHelper PATH_HELPER = new UrlPathHelper();
static {
- urlPathHelper.setUrlDecode(false);
+ PATH_HELPER.setUrlDecode(false);
}
+
private boolean useJaf = true;
private boolean ignoreUnknownExtensions = true;
/**
- * Create an instance with the given extension-to-MediaType lookup.
- * @throws IllegalArgumentException if a media type string cannot be parsed
+ * Create an instance with the given map of file extensions and media types.
*/
public PathExtensionContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes);
@@ -87,21 +89,16 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
/**
- * Indicate whether to use the Java Activation Framework to map from file
- * extensions to media types.
- *
- * <p>Default is {@code true}, i.e. the Java Activation Framework is used
- * (if available).
+ * 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.
*/
public void setUseJaf(boolean useJaf) {
this.useJaf = useJaf;
}
/**
- * Whether to ignore requests that have a file extension that does not match
- * any mapped media types. Setting this to {@code false} will result in a
- * {@code HttpMediaTypeNotAcceptableException}.
- *
+ * Whether to ignore requests with unknown file extension. Setting this to
+ * {@code false} results in {@code HttpMediaTypeNotAcceptableException}.
* <p>By default this is set to {@code true}.
*/
public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) {
@@ -111,35 +108,31 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
@Override
protected String getMediaTypeKey(NativeWebRequest webRequest) {
- HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
- if (servletRequest == null) {
+ HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+ if (request == null) {
logger.warn("An HttpServletRequest is required to determine the media type key");
return null;
}
- String path = urlPathHelper.getLookupPathForRequest(servletRequest);
+ 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;
}
@Override
- protected void handleMatch(String extension, MediaType mediaType) {
- }
-
- @Override
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
throws HttpMediaTypeNotAcceptableException {
if (this.useJaf && JAF_PRESENT) {
- MediaType jafMediaType = JafMediaTypeFactory.getMediaType("file." + extension);
- if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) {
- return jafMediaType;
+ MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + extension);
+ if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
+ return mediaType;
}
}
- if (!this.ignoreUnknownExtensions) {
- throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
+ if (this.ignoreUnknownExtensions) {
+ return null;
}
- return null;
+ throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
}
@@ -161,7 +154,7 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
Resource resource = new ClassPathResource("org/springframework/mail/javamail/mime.types");
if (resource.exists()) {
if (logger.isTraceEnabled()) {
- logger.trace("Loading Java Activation Framework FileTypeMap from " + resource);
+ logger.trace("Loading JAF FileTypeMap from " + resource);
}
InputStream inputStream = null;
try {
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 01aa7cda..20338710 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -25,43 +25,42 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
- * An extension of {@code PathExtensionContentNegotiationStrategy} that uses
- * {@link ServletContext#getMimeType(String)} as a fallback mechanism when
- * matching a path extension to a media type.
+ * Extends {@code PathExtensionContentNegotiationStrategy} that also uses
+ * {@link ServletContext#getMimeType(String)} to resolve file extensions.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class ServletPathExtensionContentNegotiationStrategy extends PathExtensionContentNegotiationStrategy {
+public class ServletPathExtensionContentNegotiationStrategy
+ extends PathExtensionContentNegotiationStrategy {
private final ServletContext servletContext;
/**
* Create an instance with the given extension-to-MediaType lookup.
- * @throws IllegalArgumentException if a media type string cannot be parsed
*/
- public ServletPathExtensionContentNegotiationStrategy(
- ServletContext servletContext, Map<String, MediaType> mediaTypes) {
+ public ServletPathExtensionContentNegotiationStrategy(ServletContext context,
+ Map<String, MediaType> mediaTypes) {
super(mediaTypes);
- Assert.notNull(servletContext, "ServletContext is required!");
- this.servletContext = servletContext;
+ Assert.notNull(context, "ServletContext is required!");
+ this.servletContext = context;
}
/**
* Create an instance without any mappings to start with. Mappings may be
- * added later on if any extensions are resolved through
- * {@link ServletContext#getMimeType(String)} or through the Java Activation
- * framework.
+ * added later when extensions are resolved through
+ * {@link ServletContext#getMimeType(String)} or via JAF.
*/
- public ServletPathExtensionContentNegotiationStrategy(ServletContext servletContext) {
- this(servletContext, null);
+ public ServletPathExtensionContentNegotiationStrategy(ServletContext context) {
+ this(context, null);
}
+
/**
- * Look up the given extension via {@link ServletContext#getMimeType(String)}
- * and if that doesn't help, delegate to the parent implementation.
+ * Resolve file extension via {@link ServletContext#getMimeType(String)}
+ * and also delegate to base class for a potential JAF lookup.
*/
@Override
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
diff --git a/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java b/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java
new file mode 100644
index 00000000..8df32297
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/MissingPathVariableException.java
@@ -0,0 +1,71 @@
+/*
+ * 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.web.bind;
+
+import org.springframework.core.MethodParameter;
+
+/**
+ * {@link ServletRequestBindingException} subclass that indicates that a path
+ * variable expected in the method parameters of an {@code @RequestMapping}
+ * method is not present among the URI variables extracted from the URL.
+ * Typically that means the URI template does not match the path variable name
+ * declared on the method parameter.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+@SuppressWarnings("serial")
+public class MissingPathVariableException extends ServletRequestBindingException {
+
+ private final String variableName;
+
+ private final MethodParameter parameter;
+
+
+ /**
+ * Constructor for MissingPathVariableException.
+ * @param variableName the name of the missing path variable
+ * @param parameter the method parameter
+ */
+ public MissingPathVariableException(String variableName, MethodParameter parameter) {
+ super("");
+ this.variableName = variableName;
+ this.parameter = parameter;
+ }
+
+
+ @Override
+ public String getMessage() {
+ return "Missing URI template variable '" + this.variableName +
+ "' for method parameter of type " + this.parameter.getParameterType().getSimpleName();
+ }
+
+ /**
+ * Return the expected name of the path variable.
+ */
+ public final String getVariableName() {
+ return this.variableName;
+ }
+
+ /**
+ * Return the method parameter bound to the path variable.
+ */
+ public final MethodParameter getParameter() {
+ return this.parameter;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java b/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java
index c600059c..cb274152 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/UnsatisfiedServletRequestParameterException.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.
@@ -16,9 +16,13 @@
package org.springframework.web.bind;
+import java.util.Arrays;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -34,7 +38,7 @@ import org.springframework.util.StringUtils;
@SuppressWarnings("serial")
public class UnsatisfiedServletRequestParameterException extends ServletRequestBindingException {
- private final String[] paramConditions;
+ private final List<String[]> paramConditions;
private final Map<String, String[]> actualParams;
@@ -46,6 +50,21 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
*/
public UnsatisfiedServletRequestParameterException(String[] paramConditions, Map<String, String[]> actualParams) {
super("");
+ this.paramConditions = Arrays.<String[]>asList(paramConditions);
+ this.actualParams = actualParams;
+ }
+
+ /**
+ * Create a new UnsatisfiedServletRequestParameterException.
+ * @param paramConditions all sets of parameter conditions that have been violated
+ * @param actualParams the actual parameter Map associated with the ServletRequest
+ * @since 4.2
+ */
+ public UnsatisfiedServletRequestParameterException(List<String[]> paramConditions,
+ Map<String, String[]> actualParams) {
+
+ super("");
+ Assert.isTrue(!CollectionUtils.isEmpty(paramConditions));
this.paramConditions = paramConditions;
this.actualParams = actualParams;
}
@@ -53,27 +72,37 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
@Override
public String getMessage() {
- return "Parameter conditions \"" + StringUtils.arrayToDelimitedString(this.paramConditions, ", ") +
- "\" not met for actual request parameters: " + requestParameterMapToString(this.actualParams);
- }
-
- private static String requestParameterMapToString(Map<String, String[]> actualParams) {
- StringBuilder result = new StringBuilder();
- for (Iterator<Map.Entry<String, String[]>> it = actualParams.entrySet().iterator(); it.hasNext();) {
- Map.Entry<String, String[]> entry = it.next();
- result.append(entry.getKey()).append('=').append(ObjectUtils.nullSafeToString(entry.getValue()));
- if (it.hasNext()) {
- result.append(", ");
+ StringBuilder sb = new StringBuilder("Parameter conditions ");
+ int i = 0;
+ for (String[] conditions : this.paramConditions) {
+ if (i > 0) {
+ sb.append(" OR ");
}
+ sb.append("\"");
+ sb.append(StringUtils.arrayToDelimitedString(conditions, ", "));
+ sb.append("\"");
+ i++;
}
- return result.toString();
+ sb.append(" not met for actual request parameters: ");
+ sb.append(requestParameterMapToString(this.actualParams));
+ return sb.toString();
}
/**
- * Return the parameter conditions that have been violated.
+ * Return the parameter conditions that have been violated or the first group
+ * in case of multiple groups.
* @see org.springframework.web.bind.annotation.RequestMapping#params()
*/
public final String[] getParamConditions() {
+ return this.paramConditions.get(0);
+ }
+
+ /**
+ * Return all parameter condition groups that have been violated.
+ * @see org.springframework.web.bind.annotation.RequestMapping#params()
+ * @since 4.2
+ */
+ public final List<String[]> getParamConditionGroups() {
return this.paramConditions;
}
@@ -85,4 +114,17 @@ public class UnsatisfiedServletRequestParameterException extends ServletRequestB
return this.actualParams;
}
+
+ private static String requestParameterMapToString(Map<String, String[]> actualParams) {
+ StringBuilder result = new StringBuilder();
+ for (Iterator<Map.Entry<String, String[]>> it = actualParams.entrySet().iterator(); it.hasNext();) {
+ Map.Entry<String, String[]> entry = it.next();
+ result.append(entry.getKey()).append('=').append(ObjectUtils.nullSafeToString(entry.getValue()));
+ if (it.hasNext()) {
+ result.append(", ");
+ }
+ }
+ return result.toString();
+ }
+
}
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 56ff681e..77562398 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
@@ -251,7 +251,7 @@ public class WebDataBinder extends DataBinder {
* @return the empty value (for most fields: null)
*/
protected Object getEmptyValue(String field, Class<?> fieldType) {
- if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) {
+ if (fieldType != null && boolean.class == fieldType || Boolean.class == fieldType) {
// Special handling of boolean property.
return Boolean.FALSE;
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java
index 0dec918c..07ad0ac6 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -23,6 +23,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.stereotype.Component;
/**
@@ -50,6 +51,7 @@ import org.springframework.stereotype.Component;
*
* @author Rossen Stoyanchev
* @author Brian Clozel
+ * @author Sam Brannen
* @since 3.2
*/
@Target(ElementType.TYPE)
@@ -59,26 +61,28 @@ import org.springframework.stereotype.Component;
public @interface ControllerAdvice {
/**
- * Alias for the {@link #basePackages()} attribute.
- * Allows for more concise annotation declarations e.g.:
+ * 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")}.
* @since 4.0
* @see #basePackages()
*/
+ @AliasFor("basePackages")
String[] value() default {};
/**
* Array of base packages.
- * Controllers that belong to those base packages or sub-packages thereof
+ * <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
+ * <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.
* @since 4.0
*/
+ @AliasFor("value")
String[] basePackages() default {};
/**
@@ -93,7 +97,7 @@ public @interface ControllerAdvice {
/**
* Array of classes.
- * Controllers that are assignable to at least one of the given types
+ * <p>Controllers that are assignable to at least one of the given types
* will be assisted by the {@code @ControllerAdvice} annotated class.
* @since 4.0
*/
@@ -101,7 +105,7 @@ public @interface ControllerAdvice {
/**
* Array of annotations.
- * Controllers that are annotated with this/one of those annotation(s)
+ * <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}.
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java
index 6af1c621..41aafefa 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -22,14 +22,18 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
+
/**
* Annotation which indicates that a method parameter should be bound to an HTTP cookie.
- * Supported for annotated handler methods in Servlet and Portlet environments.
+ *
+ * <p>Supported for annotated handler methods in Servlet and Portlet environments.
*
* <p>The method parameter may be declared as type {@link javax.servlet.http.Cookie}
- * or as cookie value type (String, int, etc).
+ * or as cookie value type (String, int, etc.).
*
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.0
* @see RequestMapping
* @see RequestParam
@@ -45,24 +49,33 @@ import java.lang.annotation.Target;
public @interface CookieValue {
/**
- * The name of the cookie to bind to.
+ * Alias for {@link #name}.
*/
+ @AliasFor("name")
String value() default "";
/**
- * Whether the header is required.
- * <p>Default is {@code true}, leading to an exception being thrown
- * in case the header is missing in the request. Switch this to
- * {@code false} if you prefer a {@code null} in case of the
- * missing header.
- * <p>Alternatively, provide a {@link #defaultValue}, which implicitly sets
- * this flag to {@code false}.
+ * The name of the cookie to bind to.
+ * @since 4.2
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
+ * Whether the cookie is required.
+ * <p>Defaults to {@code true}, leading to an exception being thrown
+ * if the cookie is missing in the request. Switch this to
+ * {@code false} if you prefer a {@code null} value if the cookie is
+ * not present in the request.
+ * <p>Alternatively, provide a {@link #defaultValue}, which implicitly
+ * sets this flag to {@code false}.
*/
boolean required() default true;
/**
- * The default value to use as a fallback. Supplying a default value implicitly
- * sets {@link #required} to {@code false}.
+ * The default value to use as a fallback.
+ * <p>Supplying a default value implicitly sets {@link #required} to
+ * {@code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
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
new file mode 100644
index 00000000..65e92007
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java
@@ -0,0 +1,116 @@
+/*
+ * 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.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;
+
+/**
+ * Marks the annotated method or type as permitting cross origin requests.
+ *
+ * <p>By default, all origins and headers are permitted.
+ *
+ * @author Russell Allen
+ * @author Sebastien Deleuze
+ * @author Sam Brannen
+ * @since 4.2
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface CrossOrigin {
+
+ String[] DEFAULT_ORIGINS = { "*" };
+
+ String[] DEFAULT_ALLOWED_HEADERS = { "*" };
+
+ boolean DEFAULT_ALLOW_CREDENTIALS = true;
+
+ long DEFAULT_MAX_AGE = 1800;
+
+
+ /**
+ * Alias for {@link #origins}.
+ */
+ @AliasFor("origins")
+ String[] value() default {};
+
+ /**
+ * List of allowed origins, e.g. {@code "http://domain1.com"}.
+ * <p>These values are placed in the {@code Access-Control-Allow-Origin}
+ * header of both the pre-flight response and the actual response.
+ * {@code "*"} means that all origins are allowed.
+ * <p>If undefined, all origins are allowed.
+ * @see #value
+ */
+ @AliasFor("value")
+ String[] origins() default {};
+
+ /**
+ * List of request headers that can be used during the actual request.
+ * <p>This property controls the value of the pre-flight response's
+ * {@code Access-Control-Allow-Headers} header.
+ * {@code "*"} means that all headers requested by the client are allowed.
+ * <p>If undefined, all requested headers are allowed.
+ */
+ String[] allowedHeaders() default {};
+
+ /**
+ * List of response headers that the user-agent will allow the client to access.
+ * <p>This property controls the value of actual response's
+ * {@code Access-Control-Expose-Headers} header.
+ * <p>If undefined, an empty exposed header list is used.
+ */
+ String[] exposedHeaders() default {};
+
+ /**
+ * List of supported HTTP request methods, e.g.
+ * {@code "{RequestMethod.GET, RequestMethod.POST}"}.
+ * <p>Methods specified here override those specified via {@code RequestMapping}.
+ * <p>If undefined, methods defined by {@link RequestMapping} annotation
+ * are used.
+ */
+ RequestMethod[] methods() default {};
+
+ /**
+ * Whether the browser should include any cookies associated with the
+ * domain of the request being annotated.
+ * <p>Set to {@code "false"} if such cookies should not included.
+ * An empty string ({@code ""}) means <em>undefined</em>.
+ * {@code "true"} means that the pre-flight response will include the header
+ * {@code Access-Control-Allow-Credentials=true}.
+ * <p>If undefined, credentials are allowed.
+ */
+ String allowCredentials() default "";
+
+ /**
+ * The maximum age (in seconds) of the cache duration for pre-flight responses.
+ * <p>This property controls the value of the {@code Access-Control-Max-Age}
+ * header in the pre-flight response.
+ * <p>Setting this to a reasonable value can reduce the number of pre-flight
+ * request/response interactions required by the browser.
+ * A negative value means <em>undefined</em>.
+ * <p>If undefined, max age is set to {@code 1800} seconds (i.e., 30 minutes).
+ */
+ long maxAge() default -1;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java
index 1b646bbc..98cbd6a8 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ExceptionHandler.java
@@ -65,6 +65,10 @@ import java.lang.annotation.Target;
* <li>{@link java.io.OutputStream} / {@link java.io.Writer} for generating
* the response's content. This will be the raw OutputStream/Writer as
* exposed by the Servlet/Portlet API.
+ * <li>{@link org.springframework.ui.Model} as an alternative to returning
+ * a model map from the handler method. Note that the provided model is not
+ * pre-populated with regular model attributes and therefore always empty,
+ * as a convenience for preparing the model for an exception-specific view.
* </ul>
*
* <p>The following return types are supported for handler methods:
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java
index 6ae0d7e1..d531c40f 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
+
/**
* Annotation which indicates that a method parameter should be bound to a
* name-value pair within a path segment. Supported for {@link RequestMapping}
@@ -37,6 +39,7 @@ import java.lang.annotation.Target;
* matrix variable names and values.
*
* @author Rossen Stoyanchev
+ * @author Sam Brannen
* @since 3.2
*/
@Target(ElementType.PARAMETER)
@@ -45,11 +48,20 @@ import java.lang.annotation.Target;
public @interface MatrixVariable {
/**
- * The name of the matrix variable.
+ * Alias for {@link #name}.
*/
+ @AliasFor("name")
String value() default "";
/**
+ * The name of the matrix variable.
+ * @since 4.2
+ * @see #value
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
* The name of the URI path variable where the matrix variable is located,
* if necessary for disambiguation (e.g. a matrix variable with the same
* name present in more than one path segment).
@@ -58,17 +70,18 @@ public @interface MatrixVariable {
/**
* Whether the matrix variable is required.
- * <p>Default is {@code true}, leading to an exception thrown in case
- * of the variable missing in the request. Switch this to {@code false}
- * if you prefer a {@code null} in case of the variable missing.
- * <p>Alternatively, provide a {@link #defaultValue() defaultValue},
- * which implicitly sets this flag to {@code false}.
+ * <p>Default is {@code true}, leading to an exception being thrown in
+ * case the variable is missing in the request. Switch this to {@code false}
+ * if you prefer a {@code null} if the variable is missing.
+ * <p>Alternatively, provide a {@link #defaultValue}, which implicitly sets
+ * this flag to {@code false}.
*/
boolean required() default true;
/**
- * The default value to use as a fallback. Supplying a default value implicitly
- * sets {@link #required()} to false.
+ * The default value to use as a fallback.
+ * <p>Supplying a default value implicitly sets {@link #required} to
+ * {@code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java
index 7276bc34..b7203f2f 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -22,16 +22,20 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
+
/**
* Annotation which indicates that a method parameter should be bound to a web request header.
- * Supported for annotated handler methods in Servlet and Portlet environments.
*
- * <p>If the method parameter is {@link java.util.Map Map&lt;String, String&gt;} or
+ * <p>Supported for annotated handler methods in Servlet and Portlet environments.
+ *
+ * <p>If the method parameter is {@link java.util.Map Map&lt;String, String&gt;},
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, String&gt;},
* or {@link org.springframework.http.HttpHeaders HttpHeaders} then the map is
* populated with all header names and values.
*
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.0
* @see RequestMapping
* @see RequestParam
@@ -45,23 +49,33 @@ import java.lang.annotation.Target;
public @interface RequestHeader {
/**
- * The name of the request header to bind to.
+ * Alias for {@link #name}.
*/
+ @AliasFor("name")
String value() default "";
/**
+ * The name of the request header to bind to.
+ * @since 4.2
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
* Whether the header is required.
- * <p>Default is {@code true}, leading to an exception thrown in case
- * of the header missing in the request. Switch this to {@code false}
- * if you prefer a {@code null} in case of the header missing.
- * <p>Alternatively, provide a {@link #defaultValue}, which implicitly sets
- * this flag to {@code false}.
+ * <p>Defaults to {@code true}, leading to an exception being thrown
+ * if the header is missing in the request. Switch this to
+ * {@code false} if you prefer a {@code null} value if the header is
+ * not present in the request.
+ * <p>Alternatively, provide a {@link #defaultValue}, which implicitly
+ * sets this flag to {@code false}.
*/
boolean required() default true;
/**
- * The default value to use as a fallback. Supplying a default value implicitly
- * sets {@link #required} to {@code false}.
+ * The default value to use as a fallback.
+ * <p>Supplying a default value implicitly sets {@link #required} to
+ * {@code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
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 37f82130..c884775c 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
@@ -23,6 +23,8 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Callable;
+import org.springframework.core.annotation.AliasFor;
+
/**
* Annotation for mapping web requests onto specific handler classes and/or
* handler methods. Provides a consistent style between Servlet and Portlet
@@ -214,6 +216,19 @@ import java.util.concurrent.Callable;
* <li>A {@link org.springframework.util.concurrent.ListenableFuture}
* which the application uses to produce a return value in a separate
* thread of its own choosing, as an alternative to returning a Callable.
+ * <li>A {@link java.util.concurrent.CompletionStage} (implemented by
+ * {@link java.util.concurrent.CompletableFuture} for example)
+ * which the application uses to produce a return value in a separate
+ * thread of its own choosing, as an alternative to returning a Callable.
+ * <li>A {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter}
+ * can be used to write multiple objects to the response asynchronously;
+ * also supported as the body within {@code ResponseEntity}.</li>
+ * <li>An {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter}
+ * can be used to write Server-Sent Events to the response asynchronously;
+ * also supported as the body within {@code ResponseEntity}.</li>
+ * <li>A {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody}
+ * can be used to write to the response asynchronously;
+ * also supported as the body within {@code ResponseEntity}.</li>
* <li>{@code void} if the method handles the response itself (by
* writing the response content directly, declaring an argument of type
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}
@@ -285,21 +300,34 @@ public @interface RequestMapping {
/**
* The primary mapping expressed by this annotation.
- * <p>In a Servlet environment: the path mapping URIs (e.g. "/myPath.do").
- * Ant-style path patterns are also supported (e.g. "/myPath/*.do").
- * At the method level, relative paths (e.g. "edit.do") are supported
- * within the primary mapping expressed at the type level.
- * Path mapping URIs may contain placeholders (e.g. "/${connect}")
- * <p>In a Portlet environment: the mapped portlet modes
+ * <p>In a Servlet environment this is an alias for {@link #path}.
+ * For example {@code @RequestMapping("/foo")} is equivalent to
+ * {@code @RequestMapping(path="/foo")}.
+ * <p>In a Portlet environment this is the mapped portlet modes
* (i.e. "EDIT", "VIEW", "HELP" or any custom modes).
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
- * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
*/
+ @AliasFor("path")
String[] value() default {};
/**
+ * In a Servlet environment only: the path mapping URIs (e.g. "/myPath.do").
+ * Ant-style path patterns are also supported (e.g. "/myPath/*.do").
+ * At the method level, relative paths (e.g. "edit.do") are supported within
+ * the primary mapping expressed at the type level. Path mapping URIs may
+ * contain placeholders (e.g. "/${connect}")
+ * <p><b>Supported at the type level as well as at the method level!</b>
+ * When used at the type level, all method-level mappings inherit
+ * this primary mapping, narrowing it for a specific handler method.
+ * @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
+ * @since 4.2
+ */
+ @AliasFor("value")
+ String[] path() default {};
+
+ /**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* <p><b>Supported at the type level as well as at the method level!</b>
@@ -386,8 +414,11 @@ public @interface RequestMapping {
* <pre class="code">
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
+ * produces = "application/json; charset=UTF-8"
* </pre>
- * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
+ * <p>It affects the actual content type written, for example to produce a JSON response
+ * with UTF-8 encoding, {@code "application/json; charset=UTF-8"} should be used.
+ * <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Accept} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java
index b1395b49..9c5e2a03 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestParam.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.
@@ -23,10 +23,13 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Map;
+import org.springframework.core.annotation.AliasFor;
+
/**
* Annotation which indicates that a method parameter should be bound to a web
- * request parameter. Supported for annotated handler methods in Servlet and
- * Portlet environments.
+ * request parameter.
+ *
+ * <p>Supported for annotated handler methods in Servlet and Portlet environments.
*
* <p>If the method parameter type is {@link Map} and a request parameter name
* is specified, then the request parameter value is converted to a {@link Map}
@@ -39,6 +42,7 @@ import java.util.Map;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 2.5
* @see RequestMapping
* @see RequestHeader
@@ -53,24 +57,34 @@ import java.util.Map;
public @interface RequestParam {
/**
- * The name of the request parameter to bind to.
+ * Alias for {@link #name}.
*/
+ @AliasFor("name")
String value() default "";
/**
+ * The name of the request parameter to bind to.
+ * @since 4.2
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
* Whether the parameter is required.
- * <p>Default is {@code true}, leading to an exception thrown in case
- * of the parameter missing in the request. Switch this to {@code false}
- * if you prefer a {@code null} in case of the parameter missing.
- * <p>Alternatively, provide a {@link #defaultValue() defaultValue},
- * which implicitly sets this flag to {@code false}.
+ * <p>Defaults to {@code true}, leading to an exception being thrown
+ * if the parameter is missing in the request. Switch this to
+ * {@code false} if you prefer a {@code null} value if the parameter is
+ * not present in the request.
+ * <p>Alternatively, provide a {@link #defaultValue}, which implicitly
+ * sets this flag to {@code false}.
*/
boolean required() default true;
/**
- * The default value to use as a fallback when the request parameter value
- * is not provided or empty. Supplying a default value implicitly sets
- * {@link #required()} to false.
+ * The default value to use as a fallback when the request parameter is
+ * not provided or has an empty value.
+ * <p>Supplying a default value implicitly sets {@link #required} to
+ * {@code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java
index a299313d..8f27d5c8 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.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.
@@ -23,6 +23,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.core.convert.converter.Converter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.multipart.MultipartFile;
@@ -30,7 +31,9 @@ import org.springframework.web.multipart.MultipartResolver;
/**
* Annotation that can be used to associate the part of a "multipart/form-data" request
- * with a method argument. Supported method argument types include {@link MultipartFile}
+ * with a method argument.
+ *
+ * <p>Supported method argument types include {@link MultipartFile}
* in conjunction with Spring's {@link MultipartResolver} abstraction,
* {@code javax.servlet.http.Part} in conjunction with Servlet 3.0 multipart requests,
* or otherwise for any other method argument, the content of the part is passed through an
@@ -50,8 +53,8 @@ import org.springframework.web.multipart.MultipartResolver;
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
+ * @author Sam Brannen
* @since 3.1
- *
* @see RequestParam
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
*/
@@ -61,15 +64,24 @@ import org.springframework.web.multipart.MultipartResolver;
public @interface RequestPart {
/**
- * The name of the part in the "multipart/form-data" request to bind to.
+ * Alias for {@link #name}.
*/
+ @AliasFor("name")
String value() default "";
/**
+ * The name of the part in the {@code "multipart/form-data"} request to bind to.
+ * @since 4.2
+ */
+ @AliasFor("value")
+ String name() default "";
+
+ /**
* Whether the part is required.
- * <p>Default is {@code true}, leading to an exception thrown in case
- * of the part missing in the request. Switch this to {@code false}
- * if you prefer a {@code null} in case of the part missing.
+ * <p>Defaults to {@code true}, leading to an exception being thrown
+ * if the part is missing in the request. Switch this to
+ * {@code false} if you prefer a {@code null} value if the part is
+ * not present in the request.
*/
boolean required() default true;
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 e4df7821..8ad9c1e9 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-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.
@@ -22,14 +22,32 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
/**
- * Marks a method or exception class with the status code and reason that should be returned. The status code is applied
- * to the HTTP response when the handler method is invoked, or whenever said exception is thrown.
+ * Marks a method or exception class with the status {@link #code} and
+ * {@link #reason} that should be returned.
+ *
+ * <p>The status code is applied to the HTTP response when the handler
+ * method is invoked and overrides status information set by other means,
+ * like {@code ResponseEntity} or {@code "redirect:"}.
+ *
+ * <p><strong>Warning</strong>: when using this annotation on an exception
+ * class, or when setting the {@code reason} attribute of this annotation,
+ * the {@code HttpServletResponse.sendError} method will be used.
+ *
+ * <p>With {@code HttpServletResponse.sendError}, the response is considered
+ * complete and should not be written to any further. Furthermore, the Servlet
+ * container will typically write an HTML error page therefore making the
+ * use of a {@code reason} unsuitable for REST APIs. For such cases it is
+ * preferable to use a {@link org.springframework.http.ResponseEntity} as
+ * a return type and avoid the use of {@code @ResponseStatus} altogether.
*
* @author Arjen Poutsma
+ * @author Sam Brannen
* @see org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver
+ * @see javax.servlet.http.HttpServletResponse#sendError(int, String)
* @since 3.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@@ -38,17 +56,24 @@ import org.springframework.http.HttpStatus;
public @interface ResponseStatus {
/**
- * The status code to use for the response.
- *
+ * Alias for {@link #code}.
+ */
+ @AliasFor("code")
+ HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
+
+ /**
+ * The status <em>code</em> to use for the response.
+ * <p>Default is {@link HttpStatus#INTERNAL_SERVER_ERROR}, which should
+ * typically be changed to something more appropriate.
+ * @since 4.2
* @see javax.servlet.http.HttpServletResponse#setStatus(int)
+ * @see javax.servlet.http.HttpServletResponse#sendError(int)
*/
- HttpStatus value();
+ @AliasFor("value")
+ HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
/**
- * The reason to be used for the response. <p>If this element is not set, it will default to the standard status
- * message for the status code. Note that due to the use of {@code HttpServletResponse.sendError(int, String)},
- * the response will be considered complete and should not be written to any further.
- *
+ * The <em>reason</em> to be used for the response.
* @see javax.servlet.http.HttpServletResponse#sendError(int, String)
*/
String reason() default "";
diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java
index fce8db6e..fdfb9273 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttributes.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.
@@ -23,11 +23,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.core.annotation.AliasFor;
+
/**
- * Annotation that indicates the session attributes that a specific handler
- * uses. This will typically list the names of model attributes which should be
+ * Annotation that indicates the session attributes that a specific handler uses.
+ *
+ * <p>This will typically list the names of model attributes which should be
* transparently stored in the session or some conversational storage,
- * serving as form-backing beans. <b>Declared at the type level,</b> applying
+ * serving as form-backing beans. <b>Declared at the type level</b>, applying
* to the model attributes that the annotated handler class operates on.
*
* <p><b>NOTE:</b> Session attributes as indicated using this annotation
@@ -44,11 +47,12 @@ import java.lang.annotation.Target;
* generic {@link org.springframework.web.context.request.WebRequest} interface.
*
* <p><b>NOTE:</b> When using controller interfaces (e.g. for AOP proxying),
- * make sure to consistently put <i>all</i> your mapping annotations - such as
- * {@code @RequestMapping} and {@code @SessionAttributes} - on
+ * make sure to consistently put <i>all</i> your mapping annotations &mdash;
+ * such as {@code @RequestMapping} and {@code @SessionAttributes} &mdash; on
* the controller <i>interface</i> rather than on the implementation class.
*
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 2.5
*/
@Target({ElementType.TYPE})
@@ -58,18 +62,28 @@ import java.lang.annotation.Target;
public @interface SessionAttributes {
/**
- * The names of session attributes in the model, to be stored in the
- * session or some conversational storage.
- * <p>Note: This indicates the model attribute names. The session attribute
- * names may or may not match the model attribute names; applications should
- * not rely on the session attribute names but rather operate on the model only.
+ * Alias for {@link #names}.
*/
+ @AliasFor("names")
String[] value() default {};
/**
- * The types of session attributes in the model, to be stored in the
- * session or some conversational storage. All model attributes of this
- * type will be stored in the session, regardless of attribute name.
+ * The names of session attributes in the model that should be stored in the
+ * session or some conversational storage.
+ * <p><strong>Note</strong>: This indicates the <em>model attribute names</em>.
+ * The <em>session attribute names</em> may or may not match the model attribute
+ * names. Applications should therefore not rely on the session attribute
+ * names but rather operate on the model only.
+ * @since 4.2
+ */
+ @AliasFor("value")
+ String[] names() default {};
+
+ /**
+ * The types of session attributes in the model that should be stored in the
+ * session or some conversational storage.
+ * <p>All model attributes of these types will be stored in the session,
+ * regardless of attribute name.
*/
Class<?>[] types() default {};
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 d1e86e4f..eb869333 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -43,6 +43,7 @@ import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
@@ -241,7 +242,7 @@ public class HandlerMethodInvoker {
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < args.length; i++) {
- MethodParameter methodParam = new MethodParameter(handlerMethod, i);
+ MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);
methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
String paramName = null;
@@ -260,14 +261,14 @@ public class HandlerMethodInvoker {
for (Annotation paramAnn : paramAnns) {
if (RequestParam.class.isInstance(paramAnn)) {
RequestParam requestParam = (RequestParam) paramAnn;
- paramName = requestParam.value();
+ paramName = requestParam.name();
required = requestParam.required();
defaultValue = parseDefaultValueAttribute(requestParam.defaultValue());
annotationsFound++;
}
else if (RequestHeader.class.isInstance(paramAnn)) {
RequestHeader requestHeader = (RequestHeader) paramAnn;
- headerName = requestHeader.value();
+ headerName = requestHeader.name();
required = requestHeader.required();
defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue());
annotationsFound++;
@@ -278,7 +279,7 @@ public class HandlerMethodInvoker {
}
else if (CookieValue.class.isInstance(paramAnn)) {
CookieValue cookieValue = (CookieValue) paramAnn;
- cookieName = cookieValue.value();
+ cookieName = cookieValue.name();
required = cookieValue.required();
defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue());
annotationsFound++;
@@ -420,7 +421,7 @@ public class HandlerMethodInvoker {
Object[] initBinderArgs = new Object[initBinderParams.length];
for (int i = 0; i < initBinderArgs.length; i++) {
- MethodParameter methodParam = new MethodParameter(initBinderMethod, i);
+ MethodParameter methodParam = new SynthesizingMethodParameter(initBinderMethod, i);
methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
String paramName = null;
@@ -432,7 +433,7 @@ public class HandlerMethodInvoker {
for (Annotation paramAnn : paramAnns) {
if (RequestParam.class.isInstance(paramAnn)) {
RequestParam requestParam = (RequestParam) paramAnn;
- paramName = requestParam.value();
+ paramName = requestParam.name();
paramRequired = requestParam.required();
paramDefaultValue = parseDefaultValueAttribute(requestParam.defaultValue());
break;
@@ -740,7 +741,7 @@ public class HandlerMethodInvoker {
private Object checkValue(String name, Object value, Class<?> paramType) {
if (value == null) {
- if (boolean.class.equals(paramType)) {
+ if (boolean.class == paramType) {
return Boolean.FALSE;
}
else if (paramType.isPrimitive()) {
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 1848a000..9fd25c01 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -107,7 +107,7 @@ public class HandlerMethodResolver {
SessionAttributes sessionAttributes = AnnotationUtils.findAnnotation(handlerType, SessionAttributes.class);
this.sessionAttributesFound = (sessionAttributes != null);
if (this.sessionAttributesFound) {
- this.sessionAttributeNames.addAll(Arrays.asList(sessionAttributes.value()));
+ this.sessionAttributeNames.addAll(Arrays.asList(sessionAttributes.names()));
this.sessionAttributeTypes.addAll(Arrays.asList(sessionAttributes.types()));
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/SpringWebConstraintValidatorFactory.java b/spring-web/src/main/java/org/springframework/web/bind/support/SpringWebConstraintValidatorFactory.java
new file mode 100644
index 00000000..09b69663
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/support/SpringWebConstraintValidatorFactory.java
@@ -0,0 +1,69 @@
+/*
+ * 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.web.bind.support;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorFactory;
+
+import org.springframework.web.context.ContextLoader;
+import org.springframework.web.context.WebApplicationContext;
+
+/**
+ * JSR-303 {@link ConstraintValidatorFactory} implementation that delegates to
+ * the current Spring {@link WebApplicationContext} for creating autowired
+ * {@link ConstraintValidator} instances.
+ *
+ * <p>In contrast to
+ * {@link org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory},
+ * this variant is meant for declarative use in a standard {@code validation.xml} file,
+ * e.g. in combination with JAX-RS or JAX-WS.
+ *
+ * @author Juergen Hoeller
+ * @since 4.2.1
+ * @see ContextLoader#getCurrentWebApplicationContext()
+ * @see org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory
+ */
+public class SpringWebConstraintValidatorFactory implements ConstraintValidatorFactory {
+
+ @Override
+ public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
+ return getWebApplicationContext().getAutowireCapableBeanFactory().createBean(key);
+ }
+
+ // Bean Validation 1.1 releaseInstance method
+ public void releaseInstance(ConstraintValidator<?, ?> instance) {
+ getWebApplicationContext().getAutowireCapableBeanFactory().destroyBean(instance);
+ }
+
+
+ /**
+ * Retrieve the Spring {@link WebApplicationContext} to use.
+ * The default implementation returns the current {@link WebApplicationContext}
+ * as registered for the thread context class loader.
+ * @return the current WebApplicationContext (never {@code null})
+ * @see ContextLoader#getCurrentWebApplicationContext()
+ */
+ protected WebApplicationContext getWebApplicationContext() {
+ WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
+ if (wac == null) {
+ throw new IllegalStateException("No WebApplicationContext registered for current thread - " +
+ "consider overriding SpringWebConstraintValidatorFactory.getWebApplicationContext()");
+ }
+ return wac;
+ }
+
+}
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 f99a4b95..eb6b5230 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
@@ -49,7 +49,8 @@ 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.UriTemplate;
+import org.springframework.web.util.DefaultUriTemplateHandler;
+import org.springframework.web.util.UriTemplateHandler;
/**
* <strong>Spring's central class for asynchronous client-side HTTP access.</strong>
@@ -62,6 +63,11 @@ import org.springframework.web.util.UriTemplate;
* {@linkplain #setMessageConverters(List) message converters} with this
* {@code RestTemplate}.
*
+ * <p><strong>Note:</strong> by default {@code AsyncRestTemplate} relies on
+ * standard JDK facilities to establish HTTP connections. You can switch to use
+ * a different HTTP library such as Apache HttpComponents, Netty, and OkHttp by
+ * using a constructor accepting an {@link AsyncClientHttpRequestFactory}.
+ *
* <p>For more information, please refer to the {@link RestTemplate} API documentation.
*
* @author Arjen Poutsma
@@ -150,6 +156,22 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
return this.syncTemplate.getErrorHandler();
}
+ /**
+ * Set a custom {@link UriTemplateHandler} for expanding URI templates.
+ * <p>By default, RestTemplate uses {@link DefaultUriTemplateHandler}.
+ * @param handler the URI template handler to use
+ */
+ public void setUriTemplateHandler(UriTemplateHandler handler) {
+ this.syncTemplate.setUriTemplateHandler(handler);
+ }
+
+ /**
+ * Return the configured URI template handler.
+ */
+ public UriTemplateHandler getUriTemplateHandler() {
+ return this.syncTemplate.getUriTemplateHandler();
+ }
+
@Override
public RestOperations getRestOperations() {
return this.syncTemplate;
@@ -498,7 +520,7 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
public <T> ListenableFuture<T> execute(String url, HttpMethod method, AsyncRequestCallback requestCallback,
ResponseExtractor<T> responseExtractor, Object... urlVariables) throws RestClientException {
- URI expanded = new UriTemplate(url).expand(urlVariables);
+ URI expanded = getUriTemplateHandler().expand(url, urlVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
@@ -506,7 +528,7 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
public <T> ListenableFuture<T> execute(String url, HttpMethod method, AsyncRequestCallback requestCallback,
ResponseExtractor<T> responseExtractor, Map<String, ?> urlVariables) throws RestClientException {
- URI expanded = new UriTemplate(url).expand(urlVariables);
+ URI expanded = getUriTemplateHandler().expand(url, urlVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
@@ -644,7 +666,7 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe
}
return convertResponse(response);
}
- catch (IOException ex) {
+ catch (Throwable ex) {
throw new ExecutionException(ex);
}
finally {
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java
index d6621759..e684dd6a 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java
@@ -468,7 +468,7 @@ public interface RestOperations {
* with the static builder methods on {@code RequestEntity}, for instance:
* <pre class="code">
* MyRequest body = ...
- * RequestEntity request = RequestEntity.post(&quot;http://example.com/{foo}&quot;, &quot;bar&quot;).accept(MediaType.APPLICATION_JSON).body(body);
+ * RequestEntity request = RequestEntity.post(new URI(&quot;http://example.com/foo&quot;)).accept(MediaType.APPLICATION_JSON).body(body);
* ResponseEntity&lt;MyResponse&gt; response = template.exchange(request, MyResponse.class);
* </pre>
* @param requestEntity the entity to write to the request
@@ -484,7 +484,7 @@ public interface RestOperations {
* {@link ParameterizedTypeReference} is used to pass generic type information:
* <pre class="code">
* MyRequest body = ...
- * RequestEntity request = RequestEntity.post(&quot;http://example.com/{foo}&quot;, &quot;bar&quot;).accept(MediaType.APPLICATION_JSON).body(body);
+ * RequestEntity request = RequestEntity.post(new URI(&quot;http://example.com/foo&quot;)).accept(MediaType.APPLICATION_JSON).body(body);
* ParameterizedTypeReference&lt;List&lt;MyResponse&gt;&gt; myBean = new ParameterizedTypeReference&lt;List&lt;MyResponse&gt;&gt;() {};
* ResponseEntity&lt;List&lt;MyResponse&gt;&gt; response = template.exchange(request, myBean);
* </pre>
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 7b9fe9d4..8c7c0433 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
@@ -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.
@@ -51,7 +51,8 @@ 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.UriTemplate;
+import org.springframework.web.util.DefaultUriTemplateHandler;
+import org.springframework.web.util.UriTemplateHandler;
/**
* <strong>Spring's central class for synchronous client-side HTTP access.</strong>
@@ -59,6 +60,11 @@ import org.springframework.web.util.UriTemplate;
* It handles HTTP connections, leaving application code to provide URLs
* (with possible template variables) and extract results.
*
+ * <p><strong>Note:</strong> by default the RestTemplate relies on standard JDK
+ * facilities to establish HTTP connections. You can switch to use a different
+ * HTTP library such as Apache HttpComponents, Netty, and OkHttp through the
+ * {@link #setRequestFactory} property.
+ *
* <p>The main entry points of this template are the methods named after the six main HTTP methods:
* <table>
* <tr><th>HTTP method</th><th>RestTemplate methods</th></tr>
@@ -135,6 +141,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
+ private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();
+
private final ResponseExtractor<HttpHeaders> headersExtractor = new HeadersExtractor();
@@ -228,6 +236,23 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
return this.errorHandler;
}
+ /**
+ * Set a custom {@link UriTemplateHandler} for expanding URI templates.
+ * <p>By default, RestTemplate uses {@link DefaultUriTemplateHandler}.
+ * @param handler the URI template handler to use
+ */
+ public void setUriTemplateHandler(UriTemplateHandler handler) {
+ Assert.notNull(handler, "UriTemplateHandler must not be null");
+ this.uriTemplateHandler = handler;
+ }
+
+ /**
+ * Return the configured URI template handler.
+ */
+ public UriTemplateHandler getUriTemplateHandler() {
+ return this.uriTemplateHandler;
+ }
+
// GET
@@ -528,7 +553,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor, Object... urlVariables) throws RestClientException {
- URI expanded = new UriTemplate(url).expand(urlVariables);
+ URI expanded = getUriTemplateHandler().expand(url, urlVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
@@ -536,7 +561,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor, Map<String, ?> urlVariables) throws RestClientException {
- URI expanded = new UriTemplate(url).expand(urlVariables);
+ URI expanded = getUriTemplateHandler().expand(url, urlVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
@@ -748,7 +773,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
if (!requestHeaders.isEmpty()) {
httpHeaders.putAll(requestHeaders);
}
- if (httpHeaders.getContentLength() == -1) {
+ if (httpHeaders.getContentLength() < 0) {
httpHeaders.setContentLength(0L);
}
}
@@ -796,7 +821,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private final HttpMessageConverterExtractor<T> delegate;
public ResponseEntityResponseExtractor(Type responseType) {
- if (responseType != null && !Void.class.equals(responseType)) {
+ if (responseType != null && Void.class != responseType) {
this.delegate = new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
}
else {
diff --git a/spring-web/src/main/java/org/springframework/web/context/AbstractContextLoaderInitializer.java b/spring-web/src/main/java/org/springframework/web/context/AbstractContextLoaderInitializer.java
index ecbf5a91..49622568 100644
--- a/spring-web/src/main/java/org/springframework/web/context/AbstractContextLoaderInitializer.java
+++ b/spring-web/src/main/java/org/springframework/web/context/AbstractContextLoaderInitializer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -22,6 +22,7 @@ import javax.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.context.ApplicationContextInitializer;
import org.springframework.web.WebApplicationInitializer;
/**
@@ -34,6 +35,7 @@ import org.springframework.web.WebApplicationInitializer;
*
* @author Arjen Poutsma
* @author Chris Beams
+ * @author Juergen Hoeller
* @since 3.2
*/
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {
@@ -56,7 +58,9 @@ public abstract class AbstractContextLoaderInitializer implements WebApplication
protected void registerContextLoaderListener(ServletContext servletContext) {
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
- servletContext.addListener(new ContextLoaderListener(rootAppContext));
+ ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
+ listener.setContextInitializers(getRootApplicationContextInitializers());
+ servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
@@ -77,4 +81,15 @@ public abstract class AbstractContextLoaderInitializer implements WebApplication
*/
protected abstract WebApplicationContext createRootApplicationContext();
+ /**
+ * Specify application context initializers to be applied to the root application
+ * context that the {@code ContextLoaderListener} is being created with.
+ * @since 4.2
+ * @see #createRootApplicationContext()
+ * @see ContextLoaderListener#setContextInitializers
+ */
+ protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
+ return null;
+ }
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/context/ContextLoader.java b/spring-web/src/main/java/org/springframework/web/context/ContextLoader.java
index 1edc6336..595d2908 100644
--- a/spring-web/src/main/java/org/springframework/web/context/ContextLoader.java
+++ b/spring-web/src/main/java/org/springframework/web/context/ContextLoader.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.
@@ -49,19 +49,18 @@ import org.springframework.util.StringUtils;
* Performs the actual initialization work for the root application context.
* Called by {@link ContextLoaderListener}.
*
- * <p>Looks for a {@link #CONTEXT_CLASS_PARAM "contextClass"} parameter
- * at the {@code web.xml} context-param level to specify the context
- * class type, falling back to the default of
- * {@link org.springframework.web.context.support.XmlWebApplicationContext}
+ * <p>Looks for a {@link #CONTEXT_CLASS_PARAM "contextClass"} parameter at the
+ * {@code web.xml} context-param level to specify the context class type, falling
+ * back to {@link org.springframework.web.context.support.XmlWebApplicationContext}
* if not found. With the default ContextLoader implementation, any context class
- * specified needs to implement the ConfigurableWebApplicationContext interface.
+ * specified needs to implement the {@link ConfigurableWebApplicationContext} interface.
*
- * <p>Processes a {@link #CONFIG_LOCATION_PARAM "contextConfigLocation"}
- * context-param and passes its value to the context instance, parsing it into
- * potentially multiple file paths which can be separated by any number of
- * commas and spaces, e.g. "WEB-INF/applicationContext1.xml,
- * WEB-INF/applicationContext2.xml". Ant-style path patterns are supported as well,
- * e.g. "WEB-INF/*Context.xml,WEB-INF/spring*.xml" or "WEB-INF/&#42;&#42;/*Context.xml".
+ * <p>Processes a {@link #CONFIG_LOCATION_PARAM "contextConfigLocation"} context-param
+ * and passes its value to the context instance, parsing it into potentially multiple
+ * file paths which can be separated by any number of commas and spaces, e.g.
+ * "WEB-INF/applicationContext1.xml, WEB-INF/applicationContext2.xml".
+ * Ant-style path patterns are supported as well, e.g.
+ * "WEB-INF/*Context.xml,WEB-INF/spring*.xml" or "WEB-INF/&#42;&#42;/*Context.xml".
* If not explicitly specified, the context implementation is supposed to use a
* default location (with XmlWebApplicationContext: "/WEB-INF/applicationContext.xml").
*
@@ -70,10 +69,9 @@ import org.springframework.util.StringUtils;
* Spring's default ApplicationContext implementations. This can be leveraged
* to deliberately override certain bean definitions via an extra XML file.
*
- * <p>Above and beyond loading the root application context, this class
- * can optionally load or obtain and hook up a shared parent context to
- * the root application context. See the
- * {@link #loadParentContext(ServletContext)} method for more information.
+ * <p>Above and beyond loading the root application context, this class can optionally
+ * load or obtain and hook up a shared parent context to the root application context.
+ * See the {@link #loadParentContext(ServletContext)} method for more information.
*
* <p>As of Spring 3.1, {@code ContextLoader} supports injecting the root web
* application context via the {@link #ContextLoader(WebApplicationContext)}
@@ -205,6 +203,10 @@ public class ContextLoader {
*/
private BeanFactoryReference parentContextRef;
+ /** Actual ApplicationContextInitializer instances to apply to the context */
+ private final List<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers =
+ new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
+
/**
* Create a new {@code ContextLoader} that will create a web application context
@@ -261,6 +263,24 @@ public class ContextLoader {
this.context = context;
}
+
+ /**
+ * Specify which {@link ApplicationContextInitializer} instances should be used
+ * to initialize the application context used by this {@code ContextLoader}.
+ * @since 4.2
+ * @see #configureAndRefreshWebApplicationContext
+ * @see #customizeContext
+ */
+ @SuppressWarnings("unchecked")
+ public void setContextInitializers(ApplicationContextInitializer<?>... initializers) {
+ if (initializers != null) {
+ for (ApplicationContextInitializer<?> initializer : initializers) {
+ this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer);
+ }
+ }
+ }
+
+
/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
@@ -391,16 +411,6 @@ public class ContextLoader {
}
}
- /**
- * @deprecated as of Spring 3.1 in favor of
- * {@link #createWebApplicationContext(ServletContext)} and
- * {@link #configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext, ServletContext)}
- */
- @Deprecated
- protected WebApplicationContext createWebApplicationContext(ServletContext sc, ApplicationContext parent) {
- return createWebApplicationContext(sc);
- }
-
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
@@ -454,29 +464,22 @@ public class ContextLoader {
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
determineContextInitializerClasses(sc);
- if (initializerClasses.isEmpty()) {
- // no ApplicationContextInitializers have been declared -> nothing to do
- return;
- }
-
- ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances =
- new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
- if (initializerContextClass != null) {
- Assert.isAssignable(initializerContextClass, wac.getClass(), String.format(
- "Could not add context initializer [%s] since its generic parameter [%s] " +
+ if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
+ throw new ApplicationContextException(String.format(
+ "Could not apply context initializer [%s] since its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
- "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(),
+ "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
wac.getClass().getName()));
}
- initializerInstances.add(BeanUtils.instantiateClass(initializerClass));
+ this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
}
- AnnotationAwareOrderComparator.sort(initializerInstances);
- for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) {
+ AnnotationAwareOrderComparator.sort(this.contextInitializers);
+ for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/context/ContextLoader.properties b/spring-web/src/main/java/org/springframework/web/context/ContextLoader.properties
deleted file mode 100644
index 6cd24b29..00000000
--- a/spring-web/src/main/java/org/springframework/web/context/ContextLoader.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-# Default WebApplicationContext implementation class for ContextLoader.
-# Used as fallback when no explicit context implementation has been specified as context-param.
-# Not meant to be customized by application developers.
-
-org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
diff --git a/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java b/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java
index 42bcd77f..3b4fa7f2 100644
--- a/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java
+++ b/spring-web/src/main/java/org/springframework/web/context/ContextLoaderListener.java
@@ -34,6 +34,7 @@ import javax.servlet.ServletContextListener;
* @author Juergen Hoeller
* @author Chris Beams
* @since 17.02.2003
+ * @see #setContextInitializers
* @see org.springframework.web.WebApplicationInitializer
* @see org.springframework.web.util.Log4jConfigListener
*/
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java
index 18983a14..61d9bdf4 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/FacesWebRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -155,6 +155,16 @@ public class FacesWebRequest extends FacesRequestAttributes implements NativeWeb
return false;
}
+ /**
+ * Last-modified handling not supported for portlet requests:
+ * As a consequence, this method always returns {@code false}.
+ * @since 4.2
+ */
+ @Override
+ public boolean checkNotModified(String etag, long lastModifiedTimestamp) {
+ return false;
+ }
+
@Override
public String getDescription(boolean includeClientInfo) {
ExternalContext externalContext = getExternalContext();
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/Log4jNestedDiagnosticContextInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/Log4jNestedDiagnosticContextInterceptor.java
index cdc61780..92248680 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/Log4jNestedDiagnosticContextInterceptor.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/Log4jNestedDiagnosticContextInterceptor.java
@@ -30,7 +30,10 @@ import org.springframework.ui.ModelMap;
* @since 2.5
* @see org.apache.log4j.NDC#push(String)
* @see org.apache.log4j.NDC#pop()
+ * @deprecated as of Spring 4.2.1, in favor of Apache Log4j 2
+ * (following Apache's EOL declaration for log4j 1.x)
*/
+@Deprecated
public class Log4jNestedDiagnosticContextInterceptor implements AsyncWebRequestInterceptor {
/** Logger available to subclasses */
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 079314f6..6cc8e052 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-2014 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.
@@ -26,6 +26,8 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -35,6 +37,8 @@ import org.springframework.web.util.WebUtils;
* {@link WebRequest} adapter for an {@link javax.servlet.http.HttpServletRequest}.
*
* @author Juergen Hoeller
+ * @author Brian Clozel
+ * @author Markus Malkusch
* @since 2.0
*/
public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
@@ -52,6 +56,10 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
private static final String METHOD_HEAD = "HEAD";
+ /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
+ private static final boolean servlet3Present =
+ ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class);
+
private boolean notModified = false;
@@ -93,13 +101,12 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return WebUtils.getNativeResponse(getResponse(), requiredType);
}
-
/**
* Return the HTTP method of the request.
* @since 4.0.2
*/
public HttpMethod getHttpMethod() {
- return HttpMethod.valueOf(getRequest().getMethod().trim().toUpperCase());
+ return HttpMethod.resolve(getRequest().getMethod());
}
@Override
@@ -168,39 +175,22 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
return getRequest().isSecure();
}
+
@Override
- @SuppressWarnings("deprecation")
public boolean checkNotModified(long lastModifiedTimestamp) {
HttpServletResponse response = getResponse();
- if (lastModifiedTimestamp >= 0 && !this.notModified &&
- (response == null || !response.containsHeader(HEADER_LAST_MODIFIED))) {
- long ifModifiedSince = -1;
- try {
- ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE);
- }
- catch (IllegalArgumentException ex) {
- String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE);
- // 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);
+ if (lastModifiedTimestamp >= 0 && !this.notModified) {
+ if (isCompatibleWithConditionalRequests(response)) {
+ this.notModified = isTimestampNotModified(lastModifiedTimestamp);
+ if (response != null) {
+ if (this.notModified && supportsNotModifiedStatus()) {
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
- catch (IllegalArgumentException ex2) {
- // Giving up
+ if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
+ response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
}
}
}
- this.notModified = (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
- if (response != null) {
- if (this.notModified && supportsNotModifiedStatus()) {
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
- }
- else {
- response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
- }
- }
}
return this.notModified;
}
@@ -208,31 +198,125 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
@Override
public boolean checkNotModified(String etag) {
HttpServletResponse response = getResponse();
- if (StringUtils.hasLength(etag) && !this.notModified &&
- (response == null || !response.containsHeader(HEADER_ETAG))) {
- String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH);
- this.notModified = etag.equals(ifNoneMatch);
- if (response != null) {
- if (this.notModified && supportsNotModifiedStatus()) {
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ if (StringUtils.hasLength(etag) && !this.notModified) {
+ if (isCompatibleWithConditionalRequests(response)) {
+ etag = addEtagPadding(etag);
+ this.notModified = isEtagNotModified(etag);
+ if (response != null) {
+ if (this.notModified && supportsNotModifiedStatus()) {
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ }
+ if (isHeaderAbsent(response, HEADER_ETAG)) {
+ response.setHeader(HEADER_ETAG, etag);
+ }
}
- else {
- response.setHeader(HEADER_ETAG, etag);
+ }
+ }
+ return this.notModified;
+ }
+
+ @Override
+ public boolean checkNotModified(String etag, long lastModifiedTimestamp) {
+ HttpServletResponse response = getResponse();
+ if (StringUtils.hasLength(etag) && !this.notModified) {
+ if (isCompatibleWithConditionalRequests(response)) {
+ etag = addEtagPadding(etag);
+ this.notModified = isEtagNotModified(etag) && 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 (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) {
+ response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp);
+ }
}
}
}
return this.notModified;
}
+ public boolean isNotModified() {
+ return this.notModified;
+ }
+
+
+ private boolean isCompatibleWithConditionalRequests(HttpServletResponse response) {
+ try {
+ if (response == null || !servlet3Present) {
+ // Can't check response.getStatus() - let's assume we're good
+ return true;
+ }
+ return HttpStatus.valueOf(response.getStatus()).is2xxSuccessful();
+ } catch (IllegalArgumentException e) {
+ return true;
+ }
+ }
+
+ private boolean isHeaderAbsent(HttpServletResponse response, String header) {
+ if (response == null || !servlet3Present) {
+ // Can't check response.getHeader(header) - let's assume it's not set
+ return true;
+ }
+ return (response.getHeader(header) == null);
+ }
+
private boolean supportsNotModifiedStatus() {
String method = getRequest().getMethod();
return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method));
}
- public boolean isNotModified() {
- return this.notModified;
+ @SuppressWarnings("deprecation")
+ private boolean isTimestampNotModified(long lastModifiedTimestamp) {
+ long ifModifiedSince = -1;
+ try {
+ ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE);
+ }
+ catch (IllegalArgumentException ex) {
+ String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE);
+ // 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);
+ }
+ catch (IllegalArgumentException ex2) {
+ // Giving up
+ }
+ }
+ }
+ return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
}
+ 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;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private String addEtagPadding(String etag) {
+ if (!(etag.startsWith("\"") || etag.startsWith("W/\"")) || !etag.endsWith("\"")) {
+ etag = "\"" + etag + "\"";
+ }
+ 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 40145149..cea9fa70 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-2014 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.
@@ -27,6 +27,7 @@ import java.util.Map;
* not for actual handling of the request.
*
* @author Juergen Hoeller
+ * @author Brian Clozel
* @since 2.0
* @see WebRequestInterceptor
*/
@@ -141,9 +142,12 @@ public interface WebRequest extends RequestAttributes {
* model.addAttribute(...);
* return "myViewName";
* }</pre>
- * <p><strong>Note:</strong> that you typically want to use either
+ * <p><strong>Note:</strong> you can use either
* this {@code #checkNotModified(long)} method; or
- * {@link #checkNotModified(String)}, but not both.
+ * {@link #checkNotModified(String)}. If you want enforce both
+ * a strong entity tag and a Last-Modified value,
+ * as recommended by the HTTP specification,
+ * then you should use {@link #checkNotModified(String, long)}.
* <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.
@@ -172,9 +176,12 @@ public interface WebRequest extends RequestAttributes {
* model.addAttribute(...);
* return "myViewName";
* }</pre>
- * <p><strong>Note:</strong> that you typically want to use either
+ * <p><strong>Note:</strong> you can use either
* this {@code #checkNotModified(String)} method; or
- * {@link #checkNotModified(long)}, but not both.
+ * {@link #checkNotModified(long)}. If you want enforce both
+ * a strong entity tag and a Last-Modified value,
+ * as recommended by the HTTP specification,
+ * then you should use {@link #checkNotModified(String, long)}.
* @param etag the entity tag that the application determined
* for the underlying resource. This parameter will be padded
* with quotes (") if necessary.
@@ -185,6 +192,41 @@ public interface WebRequest extends RequestAttributes {
boolean checkNotModified(String etag);
/**
+ * Check whether the request qualifies as not 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.
+ * <p>Typical usage:
+ * <pre class="code">
+ * public String myHandleMethod(WebRequest webRequest, Model model) {
+ * String eTag = // application-specific calculation
+ * long lastModified = // application-specific calculation
+ * if (request.checkNotModified(eTag, lastModified)) {
+ * // shortcut exit - no further processing necessary
+ * return null;
+ * }
+ * // further request processing, actually building content
+ * model.addAttribute(...);
+ * return "myViewName";
+ * }</pre>
+ * <p><strong>Note:</strong> The HTTP specification recommends
+ * setting both ETag and Last-Modified values, but you can also
+ * use {@code #checkNotModified(String)} or
+ * {@link #checkNotModified(long)}.
+ * @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
+ * @since 4.2
+ */
+ boolean checkNotModified(String etag, long lastModifiedTimestamp);
+
+ /**
* Get a short description of this request,
* typically containing request URI and session id.
* @param includeClientInfo whether to include client-specific
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java
index 28314f56..5ce98714 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResult.java
@@ -77,9 +77,12 @@ public class DeferredResult<T> {
/**
* Create a DeferredResult with a timeout value.
+ * <p>By default not set in which case the default configured in the MVC
+ * Java Config or the MVC namespace is used, or if that's not set, then the
+ * timeout depends on the default of the underlying server.
* @param timeout timeout value in milliseconds
*/
- public DeferredResult(long timeout) {
+ public DeferredResult(Long timeout) {
this(timeout, RESULT_NONE);
}
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 3adcba96..638068a3 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
@@ -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.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.web.context.request.async;
import org.springframework.web.context.request.NativeWebRequest;
@@ -45,21 +46,18 @@ public interface DeferredResultProcessingInterceptor {
* Invoked immediately before the start of concurrent handling, in the same
* thread that started it. This method may be used to capture state just prior
* to the start of concurrent processing with the given {@code DeferredResult}.
- *
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @throws Exception in case of errors
*/
- <T> void beforeConcurrentHandling(NativeWebRequest request, DeferredResult<T> deferredResult) throws Exception;
+ <T> void beforeConcurrentHandling(NativeWebRequest request, DeferredResult<T> deferredResult) throws Exception;
/**
* Invoked immediately after the start of concurrent handling, in the same
* thread that started it. This method may be used to detect the start of
* concurrent processing with the given {@code DeferredResult}.
- *
* <p>The {@code DeferredResult} may have already been set, for example at
* the time of its creation or by another thread.
- *
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @throws Exception in case of errors
@@ -71,11 +69,9 @@ public interface DeferredResultProcessingInterceptor {
* {@link DeferredResult#setResult(Object)} or
* {@link DeferredResult#setErrorResult(Object)}, and is also ready to
* handle the concurrent result.
- *
* <p>This method may also be invoked after a timeout when the
* {@code DeferredResult} was created with a constructor accepting a default
* timeout result.
- *
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @param concurrentResult the result to which the {@code DeferredResult}
@@ -88,7 +84,6 @@ public interface DeferredResultProcessingInterceptor {
* the {@code DeferredResult} has been set. Implementations may invoke
* {@link DeferredResult#setResult(Object) setResult} or
* {@link DeferredResult#setErrorResult(Object) setErrorResult} to resume processing.
- *
* @param request the current request
* @param deferredResult the DeferredResult for the current request; if the
* {@code DeferredResult} is set, then concurrent processing is resumed and
@@ -103,7 +98,6 @@ public interface DeferredResultProcessingInterceptor {
* Invoked from a container thread when an async request completed for any
* reason including timeout and network error. This method is useful for
* detecting that a {@code DeferredResult} instance is no longer usable.
- *
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @throws Exception in case of errors
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 a8878e99..fc0bb983 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
@@ -134,6 +134,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void onError(AsyncEvent event) throws IOException {
+ onComplete(event);
}
@Override
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 2322d610..a20cf23d 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-2014 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.
@@ -21,6 +21,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
+import java.util.concurrent.RejectedExecutionException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
@@ -306,24 +307,30 @@ public final class WebAsyncManager {
interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, callable);
startAsyncProcessing(processingContext);
-
- this.taskExecutor.submit(new Runnable() {
- @Override
- public void run() {
- Object result = null;
- try {
- interceptorChain.applyPreProcess(asyncWebRequest, callable);
- result = callable.call();
- }
- catch (Throwable ex) {
- result = ex;
- }
- finally {
- result = interceptorChain.applyPostProcess(asyncWebRequest, callable, result);
+ try {
+ this.taskExecutor.submit(new Runnable() {
+ @Override
+ public void run() {
+ Object result = null;
+ try {
+ interceptorChain.applyPreProcess(asyncWebRequest, callable);
+ result = callable.call();
+ }
+ catch (Throwable ex) {
+ result = ex;
+ }
+ finally {
+ result = interceptorChain.applyPostProcess(asyncWebRequest, callable, result);
+ }
+ setConcurrentResultAndDispatch(result);
}
- setConcurrentResultAndDispatch(result);
- }
- });
+ });
+ }
+ catch (RejectedExecutionException ex) {
+ Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
+ setConcurrentResultAndDispatch(result);
+ throw ex;
+ }
}
private void setConcurrentResultAndDispatch(Object result) {
diff --git a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextPropertyPlaceholderConfigurer.java b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextPropertyPlaceholderConfigurer.java
deleted file mode 100644
index 720d26c0..00000000
--- a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextPropertyPlaceholderConfigurer.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2002-2012 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.support;
-
-import java.util.Properties;
-import javax.servlet.ServletContext;
-
-import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
-import org.springframework.web.context.ServletContextAware;
-
-/**
- * Subclass of {@link PropertyPlaceholderConfigurer} that resolves placeholders as
- * ServletContext init parameters (that is, {@code web.xml} context-param
- * entries).
- *
- * <p>Can be combined with "locations" and/or "properties" values in addition
- * to web.xml context-params. Alternatively, can be defined without local
- * properties, to resolve all placeholders as {@code web.xml} context-params
- * (or JVM system properties).
- *
- * <p>If a placeholder could not be resolved against the provided local
- * properties within the application, this configurer will fall back to
- * ServletContext parameters. Can also be configured to let ServletContext
- * init parameters override local properties (contextOverride=true).
- *
- * <p>Optionally supports searching for ServletContext <i>attributes</i>: If turned
- * on, an otherwise unresolvable placeholder will matched against the corresponding
- * ServletContext attribute, using its stringified value if found. This can be
- * used to feed dynamic values into Spring's placeholder resolution.
- *
- * <p>If not running within a WebApplicationContext (or any other context that
- * is able to satisfy the ServletContextAware callback), this class will behave
- * like the default PropertyPlaceholderConfigurer. This allows for keeping
- * ServletContextPropertyPlaceholderConfigurer definitions in test suites.
- *
- * @author Juergen Hoeller
- * @since 1.1.4
- * @see #setLocations
- * @see #setProperties
- * @see #setSystemPropertiesModeName
- * @see #setContextOverride
- * @see #setSearchContextAttributes
- * @see javax.servlet.ServletContext#getInitParameter(String)
- * @see javax.servlet.ServletContext#getAttribute(String)
- * @deprecated in Spring 3.1 in favor of {@link org.springframework.context.support.PropertySourcesPlaceholderConfigurer}
- * in conjunction with {@link StandardServletEnvironment}.
- */
-@Deprecated
-public class ServletContextPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer
- implements ServletContextAware {
-
- private boolean contextOverride = false;
-
- private boolean searchContextAttributes = false;
-
- private ServletContext servletContext;
-
-
- /**
- * Set whether ServletContext init parameters (and optionally also ServletContext
- * attributes) should override local properties within the application.
- * Default is "false": ServletContext settings serve as fallback.
- * <p>Note that system properties will still override ServletContext settings,
- * if the system properties mode is set to "SYSTEM_PROPERTIES_MODE_OVERRIDE".
- * @see #setSearchContextAttributes
- * @see #setSystemPropertiesModeName
- * @see #SYSTEM_PROPERTIES_MODE_OVERRIDE
- */
- public void setContextOverride(boolean contextOverride) {
- this.contextOverride = contextOverride;
- }
-
- /**
- * Set whether to search for matching a ServletContext attribute before
- * checking a ServletContext init parameter. Default is "false": only
- * checking init parameters.
- * <p>If turned on, the configurer will look for a ServletContext attribute with
- * the same name as the placeholder, and use its stringified value if found.
- * Exposure of such ServletContext attributes can be used to dynamically override
- * init parameters defined in {@code web.xml}, for example in a custom
- * context listener.
- * @see javax.servlet.ServletContext#getInitParameter(String)
- * @see javax.servlet.ServletContext#getAttribute(String)
- */
- public void setSearchContextAttributes(boolean searchContextAttributes) {
- this.searchContextAttributes = searchContextAttributes;
- }
-
- /**
- * Set the ServletContext to resolve placeholders against.
- * Will be auto-populated when running in a WebApplicationContext.
- * <p>If not set, this configurer will simply not resolve placeholders
- * against the ServletContext: It will effectively behave like a plain
- * PropertyPlaceholderConfigurer in such a scenario.
- */
- @Override
- public void setServletContext(ServletContext servletContext) {
- this.servletContext = servletContext;
- }
-
-
- @Override
- protected String resolvePlaceholder(String placeholder, Properties props) {
- String value = null;
- if (this.contextOverride && this.servletContext != null) {
- value = resolvePlaceholder(placeholder, this.servletContext, this.searchContextAttributes);
- }
- if (value == null) {
- value = super.resolvePlaceholder(placeholder, props);
- }
- if (value == null && this.servletContext != null) {
- value = resolvePlaceholder(placeholder, this.servletContext, this.searchContextAttributes);
- }
- return value;
- }
-
- /**
- * Resolves the given placeholder using the init parameters
- * and optionally also the attributes of the given ServletContext.
- * <p>Default implementation checks ServletContext attributes before
- * init parameters. Can be overridden to customize this behavior,
- * potentially also applying specific naming patterns for parameters
- * and/or attributes (instead of using the exact placeholder name).
- * @param placeholder the placeholder to resolve
- * @param servletContext the ServletContext to check
- * @param searchContextAttributes whether to search for a matching
- * ServletContext attribute
- * @return the resolved value, of null if none
- * @see javax.servlet.ServletContext#getInitParameter(String)
- * @see javax.servlet.ServletContext#getAttribute(String)
- */
- protected String resolvePlaceholder(
- String placeholder, ServletContext servletContext, boolean searchContextAttributes) {
-
- String value = null;
- if (searchContextAttributes) {
- Object attrValue = servletContext.getAttribute(placeholder);
- if (attrValue != null) {
- value = attrValue.toString();
- }
- }
- if (value == null) {
- value = servletContext.getInitParameter(placeholder);
- }
- return value;
- }
-
-}
diff --git a/spring-web/src/main/java/org/springframework/web/context/support/WebApplicationContextUtils.java b/spring-web/src/main/java/org/springframework/web/context/support/WebApplicationContextUtils.java
index 725dc9b1..ae75fbe4 100644
--- a/spring-web/src/main/java/org/springframework/web/context/support/WebApplicationContextUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/context/support/WebApplicationContextUtils.java
@@ -68,7 +68,7 @@ public abstract class WebApplicationContextUtils {
/**
- * Find the root {@link WebApplicationContext} for this web app, typically
+ * Find the root {@code WebApplicationContext} for this web app, typically
* loaded via {@link org.springframework.web.context.ContextLoaderListener}.
* <p>Will rethrow an exception that happened on root context startup,
* to differentiate between a failed context startup and no context at all.
@@ -86,7 +86,7 @@ public abstract class WebApplicationContextUtils {
}
/**
- * Find the root {@link WebApplicationContext} for this web app, typically
+ * Find the root {@code WebApplicationContext} for this web app, typically
* loaded via {@link org.springframework.web.context.ContextLoaderListener}.
* <p>Will rethrow an exception that happened on root context startup,
* to differentiate between a failed context startup and no context at all.
@@ -99,7 +99,7 @@ public abstract class WebApplicationContextUtils {
}
/**
- * Find a custom {@link WebApplicationContext} for this web app.
+ * Find a custom {@code WebApplicationContext} for this web app.
* @param sc ServletContext to find the web application context for
* @param attrName the name of the ServletContext attribute to look for
* @return the desired WebApplicationContext for this web app, or {@code null} if none
@@ -125,6 +125,40 @@ public abstract class WebApplicationContextUtils {
return (WebApplicationContext) attr;
}
+ /**
+ * Find a unique {@code WebApplicationContext} for this web app: either the
+ * root web app context (preferred) or a unique {@code WebApplicationContext}
+ * among the registered {@code ServletContext} attributes (typically coming
+ * from a single {@code DispatcherServlet} in the current web application).
+ * <p>Note that {@code DispatcherServlet}'s exposure of its context can be
+ * controlled through its {@code publishContext} property, which is {@code true}
+ * by default but can be selectively switched to only publish a single context
+ * despite multiple {@code DispatcherServlet} registrations in the web app.
+ * @param sc ServletContext to find the web application context for
+ * @return the desired WebApplicationContext for this web app, or {@code null} if none
+ * @since 4.2
+ * @see #getWebApplicationContext(ServletContext)
+ * @see ServletContext#getAttributeNames()
+ */
+ public static WebApplicationContext findWebApplicationContext(ServletContext sc) {
+ WebApplicationContext wac = getWebApplicationContext(sc);
+ if (wac == null) {
+ Enumeration<String> attrNames = sc.getAttributeNames();
+ while (attrNames.hasMoreElements()) {
+ String attrName = attrNames.nextElement();
+ Object attrValue = sc.getAttribute(attrName);
+ if (attrValue instanceof WebApplicationContext) {
+ if (wac != null) {
+ throw new IllegalStateException("No unique WebApplicationContext found: more than one " +
+ "DispatcherServlet registered with publishContext=true?");
+ }
+ wac = (WebApplicationContext) attrValue;
+ }
+ }
+ }
+ return wac;
+ }
+
/**
* Register web-specific scopes ("request", "session", "globalSession")
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
new file mode 100644
index 00000000..76daa33b
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java
@@ -0,0 +1,393 @@
+/*
+ * 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.web.cors;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A container for CORS configuration that also provides methods to check
+ * the actual or requested origin, HTTP methods, and headers.
+ *
+ * @author Sebastien Deleuze
+ * @author Rossen Stoyanchev
+ * @author Sam Brannen
+ * @since 4.2
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>
+ */
+public class CorsConfiguration {
+
+ /**
+ * Wildcard representing <em>all</em> origins, methods, or headers.
+ */
+ public static final String ALL = "*";
+
+ private List<String> allowedOrigins;
+
+ private List<String> allowedMethods;
+
+ private List<String> allowedHeaders;
+
+ private List<String> exposedHeaders;
+
+ private Boolean allowCredentials;
+
+ private Long maxAge;
+
+
+ /**
+ * Construct a new, empty {@code CorsConfiguration} instance.
+ */
+ public CorsConfiguration() {
+ }
+
+ /**
+ * Construct a new {@code CorsConfiguration} instance by copying all
+ * values from the supplied {@code CorsConfiguration}.
+ */
+ public CorsConfiguration(CorsConfiguration other) {
+ this.allowedOrigins = other.allowedOrigins;
+ this.allowedMethods = other.allowedMethods;
+ this.allowedHeaders = other.allowedHeaders;
+ this.exposedHeaders = other.exposedHeaders;
+ this.allowCredentials = other.allowCredentials;
+ this.maxAge = other.maxAge;
+ }
+
+
+ /**
+ * Combine the supplied {@code CorsConfiguration} with this one.
+ * <p>Properties of this configuration are overridden by any non-null
+ * properties of the supplied one.
+ * @return the combined {@code CorsConfiguration} or {@code this}
+ * configuration if the supplied configuration is {@code null}
+ */
+ public CorsConfiguration combine(CorsConfiguration other) {
+ if (other == null) {
+ return this;
+ }
+ CorsConfiguration config = new CorsConfiguration(this);
+ config.setAllowedOrigins(combine(this.getAllowedOrigins(), other.getAllowedOrigins()));
+ config.setAllowedMethods(combine(this.getAllowedMethods(), other.getAllowedMethods()));
+ config.setAllowedHeaders(combine(this.getAllowedHeaders(), other.getAllowedHeaders()));
+ config.setExposedHeaders(combine(this.getExposedHeaders(), other.getExposedHeaders()));
+ Boolean allowCredentials = other.getAllowCredentials();
+ if (allowCredentials != null) {
+ config.setAllowCredentials(allowCredentials);
+ }
+ Long maxAge = other.getMaxAge();
+ if (maxAge != null) {
+ config.setMaxAge(maxAge);
+ }
+ return config;
+ }
+
+ private List<String> combine(List<String> source, List<String> other) {
+ if (other == null || other.contains(ALL)) {
+ return source;
+ }
+ if (source == null || source.contains(ALL)) {
+ return other;
+ }
+ List<String> combined = new ArrayList<String>(source);
+ combined.addAll(other);
+ return combined;
+ }
+
+
+ /**
+ * Set the origins to allow, e.g. {@code "http://domain1.com"}.
+ * <p>The special value {@code "*"} allows all domains.
+ * <p>By default this is not set.
+ */
+ public void setAllowedOrigins(List<String> allowedOrigins) {
+ this.allowedOrigins = (allowedOrigins != null ? new ArrayList<String>(allowedOrigins) : null);
+ }
+
+ /**
+ * Return the configured origins to allow, possibly {@code null}.
+ * @see #addAllowedOrigin(String)
+ * @see #setAllowedOrigins(List)
+ */
+ public List<String> getAllowedOrigins() {
+ return this.allowedOrigins;
+ }
+
+ /**
+ * Add an origin to allow.
+ */
+ public void addAllowedOrigin(String origin) {
+ if (this.allowedOrigins == null) {
+ this.allowedOrigins = new ArrayList<String>();
+ }
+ this.allowedOrigins.add(origin);
+ }
+
+ /**
+ * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"},
+ * {@code "PUT"}, etc.
+ * <p>The special value {@code "*"} allows all methods.
+ * <p>If not set, only {@code "GET"} is allowed.
+ * <p>By default this is not set.
+ */
+ public void setAllowedMethods(List<String> allowedMethods) {
+ this.allowedMethods = (allowedMethods != null ? new ArrayList<String>(allowedMethods) : null);
+ }
+
+ /**
+ * Return the allowed HTTP methods, possibly {@code null} in which case
+ * only {@code "GET"} is allowed.
+ * @see #addAllowedMethod(HttpMethod)
+ * @see #addAllowedMethod(String)
+ * @see #setAllowedMethods(List)
+ */
+ public List<String> getAllowedMethods() {
+ return this.allowedMethods;
+ }
+
+ /**
+ * Add an HTTP method to allow.
+ */
+ public void addAllowedMethod(HttpMethod method) {
+ if (method != null) {
+ addAllowedMethod(method.name());
+ }
+ }
+
+ /**
+ * Add an HTTP method to allow.
+ */
+ public void addAllowedMethod(String method) {
+ if (StringUtils.hasText(method)) {
+ if (this.allowedMethods == null) {
+ this.allowedMethods = new ArrayList<String>();
+ }
+ this.allowedMethods.add(method);
+ }
+ }
+
+ /**
+ * Set the list of headers that a pre-flight request can list as allowed
+ * for use during an actual request.
+ * <p>The special value {@code "*"} allows actual requests to send any
+ * header.
+ * <p>A header name is not required to be listed if it is one of:
+ * {@code Cache-Control}, {@code Content-Language}, {@code Expires},
+ * {@code Last-Modified}, or {@code Pragma}.
+ * <p>By default this is not set.
+ */
+ public void setAllowedHeaders(List<String> allowedHeaders) {
+ this.allowedHeaders = (allowedHeaders != null ? new ArrayList<String>(allowedHeaders) : null);
+ }
+
+ /**
+ * Return the allowed actual request headers, possibly {@code null}.
+ * @see #addAllowedHeader(String)
+ * @see #setAllowedHeaders(List)
+ */
+ public List<String> getAllowedHeaders() {
+ return this.allowedHeaders;
+ }
+
+ /**
+ * Add an actual request header to allow.
+ */
+ public void addAllowedHeader(String allowedHeader) {
+ if (this.allowedHeaders == null) {
+ this.allowedHeaders = new ArrayList<String>();
+ }
+ this.allowedHeaders.add(allowedHeader);
+ }
+
+ /**
+ * Set the list of response headers other than simple headers (i.e.
+ * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
+ * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an
+ * actual response might have and can be exposed.
+ * <p>Note that {@code "*"} is not a valid exposed header value.
+ * <p>By default this is not set.
+ */
+ public void setExposedHeaders(List<String> exposedHeaders) {
+ if (exposedHeaders != null && exposedHeaders.contains(ALL)) {
+ throw new IllegalArgumentException("'*' is not a valid exposed header value");
+ }
+ this.exposedHeaders = (exposedHeaders == null ? null : new ArrayList<String>(exposedHeaders));
+ }
+
+ /**
+ * Return the configured response headers to expose, possibly {@code null}.
+ * @see #addExposedHeader(String)
+ * @see #setExposedHeaders(List)
+ */
+ public List<String> getExposedHeaders() {
+ return this.exposedHeaders;
+ }
+
+ /**
+ * Add a response header to expose.
+ * <p>Note that {@code "*"} is not a valid exposed header value.
+ */
+ public void addExposedHeader(String exposedHeader) {
+ if (ALL.equals(exposedHeader)) {
+ throw new IllegalArgumentException("'*' is not a valid exposed header value");
+ }
+ if (this.exposedHeaders == null) {
+ this.exposedHeaders = new ArrayList<String>();
+ }
+ this.exposedHeaders.add(exposedHeader);
+ }
+
+ /**
+ * Whether user credentials are supported.
+ * <p>By default this is not set (i.e. user credentials are not supported).
+ */
+ public void setAllowCredentials(Boolean allowCredentials) {
+ this.allowCredentials = allowCredentials;
+ }
+
+ /**
+ * Return the configured {@code allowCredentials} flag, possibly {@code null}.
+ * @see #setAllowCredentials(Boolean)
+ */
+ public Boolean getAllowCredentials() {
+ return this.allowCredentials;
+ }
+
+ /**
+ * Configure how long, in seconds, the response from a pre-flight request
+ * can be cached by clients.
+ * <p>By default this is not set.
+ */
+ public void setMaxAge(Long maxAge) {
+ this.maxAge = maxAge;
+ }
+
+ /**
+ * Return the configured {@code maxAge} value, possibly {@code null}.
+ * @see #setMaxAge(Long)
+ */
+ public Long getMaxAge() {
+ return this.maxAge;
+ }
+
+
+ /**
+ * Check the origin of the request against the configured allowed origins.
+ * @param requestOrigin the origin to check
+ * @return the origin to use for the response, possibly {@code null} which
+ * means the request origin is not allowed
+ */
+ public String checkOrigin(String requestOrigin) {
+ if (!StringUtils.hasText(requestOrigin)) {
+ return null;
+ }
+ if (ObjectUtils.isEmpty(this.allowedOrigins)) {
+ return null;
+ }
+
+ if (this.allowedOrigins.contains(ALL)) {
+ if (this.allowCredentials != Boolean.TRUE) {
+ return ALL;
+ }
+ else {
+ return requestOrigin;
+ }
+ }
+ for (String allowedOrigin : this.allowedOrigins) {
+ if (requestOrigin.equalsIgnoreCase(allowedOrigin)) {
+ return requestOrigin;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check the HTTP request method (or the method from the
+ * {@code Access-Control-Request-Method} header on a pre-flight request)
+ * against the configured allowed methods.
+ * @param requestMethod the HTTP request method to check
+ * @return the list of HTTP methods to list in the response of a pre-flight
+ * request, or {@code null} if the supplied {@code requestMethod} is not allowed
+ */
+ public List<HttpMethod> checkHttpMethod(HttpMethod requestMethod) {
+ if (requestMethod == null) {
+ return null;
+ }
+ List<String> allowedMethods =
+ (this.allowedMethods != null ? this.allowedMethods : new ArrayList<String>());
+ if (allowedMethods.contains(ALL)) {
+ return Collections.singletonList(requestMethod);
+ }
+ if (allowedMethods.isEmpty()) {
+ allowedMethods.add(HttpMethod.GET.name());
+ }
+ List<HttpMethod> result = new ArrayList<HttpMethod>(allowedMethods.size());
+ boolean allowed = false;
+ for (String method : allowedMethods) {
+ if (requestMethod.matches(method)) {
+ allowed = true;
+ }
+ HttpMethod resolved = HttpMethod.resolve(method);
+ if (resolved != null) {
+ result.add(resolved);
+ }
+ }
+ return (allowed ? result : null);
+ }
+
+ /**
+ * Check the supplied request headers (or the headers listed in the
+ * {@code Access-Control-Request-Headers} of a pre-flight request) against
+ * the configured allowed headers.
+ * @param requestHeaders the request headers to check
+ * @return the list of allowed headers to list in the response of a pre-flight
+ * request, or {@code null} if none of the supplied request headers is allowed
+ */
+ public List<String> checkHeaders(List<String> requestHeaders) {
+ if (requestHeaders == null) {
+ return null;
+ }
+ if (requestHeaders.isEmpty()) {
+ return Collections.emptyList();
+ }
+ if (ObjectUtils.isEmpty(this.allowedHeaders)) {
+ return null;
+ }
+
+ boolean allowAnyHeader = this.allowedHeaders.contains(ALL);
+ List<String> result = new ArrayList<String>();
+ for (String requestHeader : requestHeaders) {
+ if (StringUtils.hasText(requestHeader)) {
+ requestHeader = requestHeader.trim();
+ for (String allowedHeader : this.allowedHeaders) {
+ if (allowAnyHeader || requestHeader.equalsIgnoreCase(allowedHeader)) {
+ result.add(requestHeader);
+ break;
+ }
+ }
+ }
+ }
+ return (result.isEmpty() ? null : result);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationSource.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationSource.java
new file mode 100644
index 00000000..2fc6d9ce
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationSource.java
@@ -0,0 +1,35 @@
+/*
+ * 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.web.cors;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Interface to be implemented by classes (usually HTTP request handlers) that
+ * provides a {@link CorsConfiguration} instance based on the provided request.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public interface CorsConfigurationSource {
+
+ /**
+ * Return a {@link CorsConfiguration} based on the incoming request.
+ */
+ CorsConfiguration getCorsConfiguration(HttpServletRequest request);
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java
new file mode 100644
index 00000000..2794cb4c
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsProcessor.java
@@ -0,0 +1,50 @@
+/*
+ * 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.web.cors;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A strategy that takes a request and a {@link CorsConfiguration} and updates
+ * the response.
+ *
+ * <p>This component is not concerned with how a {@code CorsConfiguration} is
+ * selected but rather takes follow-up actions such as applying CORS validation
+ * checks and either rejecting the response or adding CORS headers to the
+ * response.
+ *
+ * @author Sebastien Deleuze
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>
+ * @see org.springframework.web.servlet.handler.AbstractHandlerMapping#setCorsProcessor
+ */
+public interface CorsProcessor {
+
+ /**
+ * Process a request given a {@code CorsConfiguration}.
+ * @param configuration the applicable CORS configuration (possibly {@code null})
+ * @param request the current request
+ * @param response the current response
+ * @return {@code false} if the request is rejected, {@code true} otherwise
+ */
+ boolean processRequest(CorsConfiguration configuration, HttpServletRequest request,
+ HttpServletResponse response) throws IOException;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java b/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java
new file mode 100644
index 00000000..c46f6956
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java
@@ -0,0 +1,48 @@
+/*
+ * 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.web.cors;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+
+/**
+ * Utility class for CORS request handling based on the
+ * <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public abstract class CorsUtils {
+
+ /**
+ * Returns {@code true} if the request is a valid CORS one.
+ */
+ public static boolean isCorsRequest(HttpServletRequest request) {
+ return (request.getHeader(HttpHeaders.ORIGIN) != null);
+ }
+
+ /**
+ * Returns {@code true} if the request is a valid CORS pre-flight one.
+ */
+ public static boolean isPreFlightRequest(HttpServletRequest request) {
+ return (isCorsRequest(request) && HttpMethod.OPTIONS.matches(request.getMethod()) &&
+ request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java
new file mode 100644
index 00000000..3a564f80
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.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.cors;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.util.WebUtils;
+
+/**
+ * The default implementation of {@link CorsProcessor}, as defined by the
+ * <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>.
+ *
+ * <p>Note that when input {@link CorsConfiguration} is {@code null}, this
+ * implementation does not reject simple or actual requests outright but simply
+ * avoid adding CORS headers to the response. CORS processing is also skipped
+ * if the response already contains CORS headers, or if the request is detected
+ * as a same-origin one.
+ *
+ * @author Sebastien Deleuze
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public class DefaultCorsProcessor implements CorsProcessor {
+
+ private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
+
+ private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
+
+
+ @Override
+ @SuppressWarnings("resource")
+ public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+
+ if (!CorsUtils.isCorsRequest(request)) {
+ return true;
+ }
+
+ ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
+ if (responseHasCors(serverResponse)) {
+ logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
+ return true;
+ }
+
+ ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
+ if (WebUtils.isSameOrigin(serverRequest)) {
+ logger.debug("Skip CORS processing: request is from same origin");
+ return true;
+ }
+
+ boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
+ if (config == null) {
+ if (preFlightRequest) {
+ rejectRequest(serverResponse);
+ return false;
+ }
+ else {
+ return true;
+ }
+ }
+
+ return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
+ }
+
+ private boolean responseHasCors(ServerHttpResponse response) {
+ try {
+ return (response.getHeaders().getAccessControlAllowOrigin() != null);
+ }
+ catch (NullPointerException npe) {
+ // SPR-11919 and https://issues.jboss.org/browse/WFLY-3474
+ return false;
+ }
+ }
+
+ /**
+ * Invoked when one of the CORS checks failed.
+ * The default implementation sets the response status to 403 and writes
+ * "Invalid CORS request" to the response.
+ */
+ protected void rejectRequest(ServerHttpResponse response) throws IOException {
+ response.setStatusCode(HttpStatus.FORBIDDEN);
+ response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
+ }
+
+ /**
+ * Handle the given request.
+ */
+ protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
+ CorsConfiguration config, boolean preFlightRequest) throws IOException {
+
+ String requestOrigin = request.getHeaders().getOrigin();
+ String allowOrigin = checkOrigin(config, requestOrigin);
+
+ HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
+ List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
+
+ List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
+ List<String> allowHeaders = checkHeaders(config, requestHeaders);
+
+ if (allowOrigin == null || allowMethods == null || (preFlightRequest && allowHeaders == null)) {
+ rejectRequest(response);
+ return false;
+ }
+
+ HttpHeaders responseHeaders = response.getHeaders();
+ responseHeaders.setAccessControlAllowOrigin(allowOrigin);
+ responseHeaders.add(HttpHeaders.VARY, HttpHeaders.ORIGIN);
+
+ if (preFlightRequest) {
+ responseHeaders.setAccessControlAllowMethods(allowMethods);
+ }
+
+ if (preFlightRequest && !allowHeaders.isEmpty()) {
+ responseHeaders.setAccessControlAllowHeaders(allowHeaders);
+ }
+
+ if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
+ responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
+ }
+
+ if (Boolean.TRUE.equals(config.getAllowCredentials())) {
+ responseHeaders.setAccessControlAllowCredentials(true);
+ }
+
+ if (preFlightRequest && config.getMaxAge() != null) {
+ responseHeaders.setAccessControlMaxAge(config.getMaxAge());
+ }
+
+ response.flush();
+ return true;
+ }
+
+ /**
+ * Check the origin and determine the origin for the response. The default
+ * implementation simply delegates to
+ * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
+ */
+ protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
+ return config.checkOrigin(requestOrigin);
+ }
+
+ /**
+ * Check the HTTP method and determine the methods for the response of a
+ * pre-flight request. The default implementation simply delegates to
+ * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
+ */
+ protected List<HttpMethod> checkMethods(CorsConfiguration config, HttpMethod requestMethod) {
+ return config.checkHttpMethod(requestMethod);
+ }
+
+ private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) {
+ return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod());
+ }
+
+ /**
+ * Check the headers and determine the headers for the response of a
+ * pre-flight request. The default implementation simply delegates to
+ * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
+ */
+ protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) {
+ return config.checkHeaders(requestHeaders);
+ }
+
+ private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) {
+ HttpHeaders headers = request.getHeaders();
+ return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<String>(headers.keySet()));
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java b/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java
new file mode 100644
index 00000000..749696ce
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java
@@ -0,0 +1,134 @@
+/*
+ * 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.web.cors;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.PathMatcher;
+import org.springframework.web.util.UrlPathHelper;
+
+/**
+ * Provide a per request {@link CorsConfiguration} instance based on a
+ * collection of {@link CorsConfiguration} mapped on path patterns.
+ *
+ * <p>Exact path mapping URIs (such as {@code "/admin"}) are supported
+ * as well as Ant-style path patterns (such as {@code "/admin/**"}).
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {
+
+ private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<String, CorsConfiguration>();
+
+ private PathMatcher pathMatcher = new AntPathMatcher();
+
+ private UrlPathHelper urlPathHelper = new UrlPathHelper();
+
+
+ /**
+ * Set the PathMatcher implementation to use for matching URL paths
+ * against registered URL patterns. Default is AntPathMatcher.
+ * @see org.springframework.util.AntPathMatcher
+ */
+ public void setPathMatcher(PathMatcher pathMatcher) {
+ Assert.notNull(pathMatcher, "PathMatcher must not be null");
+ this.pathMatcher = pathMatcher;
+ }
+
+ /**
+ * Set if URL lookup should always use the full path within the current servlet
+ * context. Else, the path within the current servlet mapping is used if applicable
+ * (that is, in the case of a ".../*" servlet mapping in web.xml).
+ * <p>Default is "false".
+ * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
+ */
+ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
+ this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
+ }
+
+ /**
+ * Set if context path and request URI should be URL-decoded. Both are returned
+ * <i>undecoded</i> by the Servlet API, in contrast to the servlet path.
+ * <p>Uses either the request encoding or the default encoding according
+ * to the Servlet spec (ISO-8859-1).
+ * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
+ */
+ public void setUrlDecode(boolean urlDecode) {
+ this.urlPathHelper.setUrlDecode(urlDecode);
+ }
+
+ /**
+ * Set if ";" (semicolon) content should be stripped from the request URI.
+ * <p>The default value is {@code true}.
+ * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
+ */
+ public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
+ this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
+ }
+
+ /**
+ * Set the UrlPathHelper to use for resolution of lookup paths.
+ * <p>Use this to override the default UrlPathHelper with a custom subclass.
+ */
+ public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
+ Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
+ this.urlPathHelper = urlPathHelper;
+ }
+
+ /**
+ * Set CORS configuration based on URL patterns.
+ */
+ public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
+ this.corsConfigurations.clear();
+ if (corsConfigurations != null) {
+ this.corsConfigurations.putAll(corsConfigurations);
+ }
+ }
+
+ /**
+ * Get the CORS configuration.
+ */
+ public Map<String, CorsConfiguration> getCorsConfigurations() {
+ return Collections.unmodifiableMap(this.corsConfigurations);
+ }
+
+ /**
+ * Register a {@link CorsConfiguration} for the specified path pattern.
+ */
+ public void registerCorsConfiguration(String path, CorsConfiguration config) {
+ this.corsConfigurations.put(path, config);
+ }
+
+
+ @Override
+ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
+ String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
+ for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
+ if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/cors/package-info.java b/spring-web/src/main/java/org/springframework/web/cors/package-info.java
new file mode 100644
index 00000000..8331aec3
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/cors/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Support for CORS (Cross-Origin Resource Sharing),
+ * based on a common {@code CorsProcessor} strategy.
+ */
+package org.springframework.web.cors;
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 0fbdbe85..607b8bc0 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
@@ -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.
@@ -255,7 +255,10 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
if (isIncludeQueryString()) {
- msg.append('?').append(request.getQueryString());
+ String queryString = request.getQueryString();
+ if (queryString != null) {
+ msg.append('?').append(queryString);
+ }
}
if (isIncludeClientInfo()) {
String client = request.getRemoteAddr();
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 ece091f7..56a7c899 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
@@ -22,6 +22,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.util.Assert;
+
/**
* Servlet Filter that allows one to specify a character encoding for requests.
* This is useful because current browsers typically do not set a character
@@ -48,6 +50,40 @@ public class CharacterEncodingFilter extends OncePerRequestFilter {
/**
+ * Create a default {@code CharacterEncodingFilter},
+ * with the encoding to be set via {@link #setEncoding}.
+ * @see #setEncoding
+ */
+ public CharacterEncodingFilter() {
+ }
+
+ /**
+ * Create a {@code CharacterEncodingFilter} for the given encoding.
+ * @param encoding the encoding to apply
+ * @since 4.2.3
+ * @see #setEncoding
+ */
+ public CharacterEncodingFilter(String encoding) {
+ this(encoding, false);
+ }
+
+ /**
+ * Create a {@code CharacterEncodingFilter} for the given encoding.
+ * @param encoding the encoding to apply
+ * @param forceEncoding whether the specified encoding is supposed to
+ * override existing request and response encodings
+ * @since 4.2.3
+ * @see #setEncoding
+ * @see #setForceEncoding
+ */
+ public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
+ Assert.hasLength(encoding, "Encoding must not be empty");
+ this.encoding = encoding;
+ this.forceEncoding = forceEncoding;
+ }
+
+
+ /**
* Set the encoding to use for requests. This encoding will be passed into a
* {@link javax.servlet.http.HttpServletRequest#setCharacterEncoding} call.
* <p>Whether this encoding will override existing request encodings
diff --git a/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java b/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java
new file mode 100644
index 00000000..8079532e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java
@@ -0,0 +1,95 @@
+/*
+ * 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.web.filter;
+
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.util.Assert;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.CorsProcessor;
+import org.springframework.web.cors.CorsUtils;
+import org.springframework.web.cors.DefaultCorsProcessor;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+/**
+ * {@link javax.servlet.Filter} that handles CORS preflight requests and intercepts CORS
+ * simple and actual requests thanks to a {@link CorsProcessor} implementation
+ * ({@link DefaultCorsProcessor} by default) in order to add the relevant CORS response
+ * headers (like {@code Access-Control-Allow-Origin}) using the provided
+ * {@link CorsConfigurationSource} (for example an {@link UrlBasedCorsConfigurationSource}
+ * instance.
+ *
+ * <p>This is an alternative to Spring MVC Java config and XML namespace CORS configuration,
+ * useful for applications depending only on spring-web (not on spring-webmvc) or for
+ * security constraints requiring CORS checks to be performed at {@link javax.servlet.Filter}
+ * level.
+ *
+ * <p>This filter could be used in conjunction with {@link DelegatingFilterProxy} in order
+ * to help with its initialization.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>
+ */
+public class CorsFilter extends OncePerRequestFilter {
+
+ private CorsProcessor processor = new DefaultCorsProcessor();
+
+ private final CorsConfigurationSource configSource;
+
+
+ /**
+ * Constructor accepting a {@link CorsConfigurationSource} used by the filter to find
+ * the {@link CorsConfiguration} to use for each incoming request.
+ * @see UrlBasedCorsConfigurationSource
+ */
+ public CorsFilter(CorsConfigurationSource configSource) {
+ this.configSource = configSource;
+ }
+
+ /**
+ * Configure a custom {@link CorsProcessor} to use to apply the matched
+ * {@link CorsConfiguration} for a request.
+ * <p>By default {@link DefaultCorsProcessor} is used.
+ */
+ public void setCorsProcessor(CorsProcessor processor) {
+ Assert.notNull(processor, "CorsProcessor must not be null");
+ this.processor = processor;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ if (CorsUtils.isCorsRequest(request)) {
+ CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
+ if (corsConfiguration != null) {
+ boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
+ if (!isValid || CorsUtils.isPreFlightRequest(request)) {
+ return;
+ }
+ }
+ }
+ filterChain.doFilter(request, response);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java b/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java
index 47899bef..ba5bb581 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/DelegatingFilterProxy.java
@@ -249,7 +249,8 @@ public class DelegatingFilterProxy extends GenericFilterBean {
if (this.delegate == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
- throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
+ throw new IllegalStateException("No WebApplicationContext found: " +
+ "no ContextLoaderListener or DispatcherServlet registered?");
}
this.delegate = initDelegate(wac);
}
@@ -288,11 +289,12 @@ public class DelegatingFilterProxy extends GenericFilterBean {
*/
protected WebApplicationContext findWebApplicationContext() {
if (this.webApplicationContext != null) {
- // the user has injected a context at construction time -> use it
+ // The user has injected a context at construction time -> use it...
if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
- if (!((ConfigurableApplicationContext)this.webApplicationContext).isActive()) {
- // the context has not yet been refreshed -> do so before returning it
- ((ConfigurableApplicationContext)this.webApplicationContext).refresh();
+ ConfigurableApplicationContext cac = (ConfigurableApplicationContext) this.webApplicationContext;
+ if (!cac.isActive()) {
+ // The context has not yet been refreshed -> do so before returning it...
+ cac.refresh();
}
}
return this.webApplicationContext;
@@ -302,7 +304,7 @@ public class DelegatingFilterProxy extends GenericFilterBean {
return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
}
else {
- return WebApplicationContextUtils.getWebApplicationContext(getServletContext());
+ return WebApplicationContextUtils.findWebApplicationContext(getServletContext());
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/filter/Log4jNestedDiagnosticContextFilter.java b/spring-web/src/main/java/org/springframework/web/filter/Log4jNestedDiagnosticContextFilter.java
index 982552fe..4691e62e 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/Log4jNestedDiagnosticContextFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/Log4jNestedDiagnosticContextFilter.java
@@ -36,7 +36,10 @@ import org.apache.log4j.NDC;
* @see #setAfterMessageSuffix
* @see org.apache.log4j.NDC#push(String)
* @see org.apache.log4j.NDC#pop()
+ * @deprecated as of Spring 4.2.1, in favor of Apache Log4j 2
+ * (following Apache's EOL declaration for log4j 1.x)
*/
+@Deprecated
public class Log4jNestedDiagnosticContextFilter extends AbstractRequestLoggingFilter {
/** Logger available to subclasses */
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 a9134d27..b3fbe838 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
@@ -17,8 +17,12 @@
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;
+import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -26,7 +30,6 @@ import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.DigestUtils;
-import org.springframework.util.StreamUtils;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
@@ -55,6 +58,8 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
private static final String DIRECTIVE_NO_STORE = "no-store";
+ private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING";
+
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
private static final boolean servlet3Present =
@@ -76,12 +81,12 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
HttpServletResponse responseToUse = response;
if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
- responseToUse = new ContentCachingResponseWrapper(response);
+ responseToUse = new HttpStreamingAwareContentCachingResponseWrapper(response, request);
}
filterChain.doFilter(request, responseToUse);
- if (!isAsyncStarted(request)) {
+ if (!isAsyncStarted(request) && !isContentCachingDisabled(request)) {
updateResponse(request, responseToUse);
}
}
@@ -90,18 +95,14 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Assert.notNull(responseWrapper, "ContentCachingResponseWrapper not found");
-
HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse();
int statusCode = responseWrapper.getStatusCode();
- byte[] body = responseWrapper.getContentAsByteArray();
if (rawResponse.isCommitted()) {
- if (body.length > 0) {
- StreamUtils.copy(body, rawResponse.getOutputStream());
- }
+ responseWrapper.copyBodyToResponse();
}
- else if (isEligibleForEtag(request, responseWrapper, statusCode, body)) {
- String responseETag = generateETagHeaderValue(body);
+ else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) {
+ String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream());
rawResponse.setHeader(HEADER_ETAG, responseETag);
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
if (responseETag.equals(requestETag)) {
@@ -115,20 +116,14 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag +
"], sending normal response");
}
- if (body.length > 0) {
- rawResponse.setContentLength(body.length);
- StreamUtils.copy(body, rawResponse.getOutputStream());
- }
+ responseWrapper.copyBodyToResponse();
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Response with status code [" + statusCode + "] not eligible for ETag");
}
- if (body.length > 0) {
- rawResponse.setContentLength(body.length);
- StreamUtils.copy(body, rawResponse.getOutputStream());
- }
+ responseWrapper.copyBodyToResponse();
}
}
@@ -143,13 +138,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
* @param request the HTTP request
* @param response the HTTP response
* @param responseStatusCode the HTTP response status code
- * @param responseBody the response body
+ * @param inputStream the response body
* @return {@code true} if eligible for ETag generation; {@code false} otherwise
*/
protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response,
- int responseStatusCode, byte[] responseBody) {
+ int responseStatusCode, InputStream inputStream) {
- if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.name().equals(request.getMethod())) {
+ if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(request.getMethod())) {
String cacheControl = null;
if (servlet3Present) {
cacheControl = response.getHeader(HEADER_CACHE_CONTROL);
@@ -164,15 +159,57 @@ 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 bytes the response body as byte array
+ * @param inputStream the response body as an InputStream
* @return the ETag header value
* @see org.springframework.util.DigestUtils
*/
- protected String generateETagHeaderValue(byte[] bytes) {
+ protected String generateETagHeaderValue(InputStream inputStream) throws IOException {
StringBuilder builder = new StringBuilder("\"0");
- DigestUtils.appendMd5DigestAsHex(bytes, builder);
+ DigestUtils.appendMd5DigestAsHex(inputStream, builder);
builder.append('"');
return builder.toString();
}
+
+ /**
+ * This method can be used to disable the content caching response wrapper
+ * of the ShallowEtagHeaderFilter. This can be done before the start of HTTP
+ * streaming for example where the response will be written to asynchronously
+ * and not in the context of a Servlet container thread.
+ * @since 4.2
+ */
+ public static void disableContentCaching(ServletRequest request) {
+ Assert.notNull(request, "ServletRequest must not be null");
+ request.setAttribute(STREAMING_ATTRIBUTE, true);
+ }
+
+ private static boolean isContentCachingDisabled(HttpServletRequest request) {
+ return (request.getAttribute(STREAMING_ATTRIBUTE) != null);
+ }
+
+
+ private static class HttpStreamingAwareContentCachingResponseWrapper extends ContentCachingResponseWrapper {
+
+ private final HttpServletRequest request;
+
+ public HttpStreamingAwareContentCachingResponseWrapper(HttpServletResponse response, HttpServletRequest request) {
+ super(response);
+ this.request = request;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ return (useRawResponse() ? getResponse().getOutputStream() : super.getOutputStream());
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ return (useRawResponse() ? getResponse().getWriter() : super.getWriter());
+ }
+
+ private boolean useRawResponse() {
+ return isContentCachingDisabled(this.request);
+ }
+ }
+
}
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 364a237b..0f857952 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
@@ -227,11 +227,6 @@ public class ControllerAdviceBean implements Ordered {
private static Set<String> initBasePackages(ControllerAdvice annotation) {
Set<String> basePackages = new LinkedHashSet<String>();
- for (String basePackage : annotation.value()) {
- if (StringUtils.hasText(basePackage)) {
- basePackages.add(adaptBasePackage(basePackage));
- }
- }
for (String basePackage : annotation.basePackages()) {
if (StringUtils.hasText(basePackage)) {
basePackages.add(adaptBasePackage(basePackage));
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 cdbf7315..33a9b291 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
@@ -25,22 +25,25 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Encapsulates information about a handler method consisting of a
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
- * Provides convenient access to method parameters, method return value, method annotations.
+ * Provides convenient access to method parameters, the method return value,
+ * method annotations, etc.
*
* <p>The class may be created with a bean instance or with a bean name (e.g. lazy-init bean,
- * prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@link HandlerMethod}
+ * prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@code HandlerMethod}
* instance with a bean instance resolved through the associated {@link BeanFactory}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.1
*/
public class HandlerMethod {
@@ -60,6 +63,8 @@ public class HandlerMethod {
private final MethodParameter[] parameters;
+ private final HandlerMethod resolvedFromHandlerMethod;
+
/**
* Create an instance from a bean instance and a method.
@@ -73,6 +78,7 @@ public class HandlerMethod {
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
this.parameters = initMethodParameters();
+ this.resolvedFromHandlerMethod = null;
}
/**
@@ -88,12 +94,13 @@ public class HandlerMethod {
this.method = bean.getClass().getMethod(methodName, parameterTypes);
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
this.parameters = initMethodParameters();
+ this.resolvedFromHandlerMethod = null;
}
/**
* Create an instance from a bean name, a method, and a {@code BeanFactory}.
* The method {@link #createWithResolvedBean()} may be used later to
- * re-create the {@code HandlerMethod} with an initialized the bean.
+ * re-create the {@code HandlerMethod} with an initialized bean.
*/
public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
Assert.hasText(beanName, "Bean name is required");
@@ -105,6 +112,7 @@ public class HandlerMethod {
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
this.parameters = initMethodParameters();
+ this.resolvedFromHandlerMethod = null;
}
/**
@@ -118,6 +126,7 @@ public class HandlerMethod {
this.method = handlerMethod.method;
this.bridgedMethod = handlerMethod.bridgedMethod;
this.parameters = handlerMethod.parameters;
+ this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
}
/**
@@ -132,6 +141,7 @@ public class HandlerMethod {
this.method = handlerMethod.method;
this.bridgedMethod = handlerMethod.bridgedMethod;
this.parameters = handlerMethod.parameters;
+ this.resolvedFromHandlerMethod = handlerMethod;
}
@@ -183,6 +193,14 @@ public class HandlerMethod {
}
/**
+ * Return the HandlerMethod from which this HandlerMethod instance was
+ * resolved via {@link #createWithResolvedBean()}.
+ */
+ public HandlerMethod getResolvedFromHandlerMethod() {
+ return this.resolvedFromHandlerMethod;
+ }
+
+ /**
* Return the HandlerMethod return type.
*/
public MethodParameter getReturnType() {
@@ -206,11 +224,14 @@ public class HandlerMethod {
/**
* Returns a single annotation on the underlying method traversing its super methods
* 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.
* @return the annotation, or {@code null} if none found
+ * @see AnnotatedElementUtils#findMergedAnnotation
*/
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
- return AnnotationUtils.findAnnotation(this.method, annotationType);
+ return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
}
/**
@@ -253,7 +274,7 @@ public class HandlerMethod {
/**
* A MethodParameter with HandlerMethod-specific behavior.
*/
- protected class HandlerMethodParameter extends MethodParameter {
+ protected class HandlerMethodParameter extends SynthesizingMethodParameter {
public HandlerMethodParameter(int index) {
super(HandlerMethod.this.bridgedMethod, index);
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 a659ff28..82acd2c3 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -17,14 +17,9 @@
package org.springframework.web.method;
import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.util.Arrays;
-import java.util.LinkedHashSet;
import java.util.Set;
-import org.springframework.core.BridgeMethodResolver;
-import org.springframework.util.ClassUtils;
-import org.springframework.util.ReflectionUtils;
+import org.springframework.core.MethodIntrospector;
import org.springframework.util.ReflectionUtils.MethodFilter;
/**
@@ -33,7 +28,9 @@ import org.springframework.util.ReflectionUtils.MethodFilter;
*
* @author Rossen Stoyanchev
* @since 3.1
+ * @deprecated as of Spring 4.2.3, in favor of the generalized and refined {@link MethodIntrospector}
*/
+@Deprecated
public abstract class HandlerMethodSelector {
/**
@@ -42,31 +39,10 @@ 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)
*/
- public static Set<Method> selectMethods(final Class<?> handlerType, final MethodFilter handlerMethodFilter) {
- final Set<Method> handlerMethods = new LinkedHashSet<Method>();
- Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
- Class<?> specificHandlerType = null;
- if (!Proxy.isProxyClass(handlerType)) {
- handlerTypes.add(handlerType);
- specificHandlerType = handlerType;
- }
- handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces()));
- for (Class<?> currentHandlerType : handlerTypes) {
- final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
- ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
- @Override
- public void doWith(Method method) {
- Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
- Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
- if (handlerMethodFilter.matches(specificMethod) &&
- (bridgedMethod == specificMethod || !handlerMethodFilter.matches(bridgedMethod))) {
- handlerMethods.add(specificMethod);
- }
- }
- }, ReflectionUtils.USER_DECLARED_METHODS);
- }
- return handlerMethods;
+ 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 be22df68..aa724396 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-2014 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.
@@ -70,7 +70,7 @@ public abstract class AbstractCookieValueMethodArgumentResolver extends Abstract
private static class CookieValueNamedValueInfo extends NamedValueInfo {
private CookieValueNamedValueInfo(CookieValue annotation) {
- super(annotation.value(), annotation.required(), annotation.defaultValue());
+ super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
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 30bbfbc0..78a3a345 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-2014 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.
@@ -20,6 +20,8 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletException;
+import org.springframework.beans.ConversionNotSupportedException;
+import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@@ -101,7 +103,18 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
- arg = binder.convertIfNecessary(arg, paramType, parameter);
+ try {
+ arg = binder.convertIfNecessary(arg, paramType, parameter);
+ }
+ catch (ConversionNotSupportedException ex) {
+ throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
+ namedValueInfo.name, parameter, ex.getCause());
+ }
+ catch (TypeMismatchException ex) {
+ throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
+ namedValueInfo.name, parameter, ex.getCause());
+
+ }
}
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
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 46af047a..5d307cba 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-2014 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.
@@ -25,12 +25,12 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.core.ExceptionDepthComparator;
+import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.method.HandlerMethodSelector;
/**
* Discovers {@linkplain ExceptionHandler @ExceptionHandler} methods in a given class,
@@ -70,7 +70,7 @@ public class ExceptionHandlerMethodResolver {
* @param handlerType the type to introspect
*/
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
- for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
+ for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
@@ -105,9 +105,8 @@ public class ExceptionHandlerMethodResolver {
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
Method oldMethod = this.mappedMethods.put(exceptionType, method);
if (oldMethod != null && !oldMethod.equals(method)) {
- throw new IllegalStateException(
- "Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" +
- oldMethod + ", " + method + "}.");
+ throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" +
+ exceptionType + "]: {" + oldMethod + ", " + method + "}");
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentConversionNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentConversionNotSupportedException.java
new file mode 100644
index 00000000..98fc3ee6
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentConversionNotSupportedException.java
@@ -0,0 +1,61 @@
+/*
+ * 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.web.method.annotation;
+
+import org.springframework.beans.ConversionNotSupportedException;
+import org.springframework.core.MethodParameter;
+
+/**
+ * A ConversionNotSupportedException raised while resolving a method argument.
+ * Provides access to the target {@link org.springframework.core.MethodParameter
+ * MethodParameter}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+@SuppressWarnings("serial")
+public class MethodArgumentConversionNotSupportedException extends ConversionNotSupportedException {
+
+ private final String name;
+
+ private final MethodParameter parameter;
+
+
+ public MethodArgumentConversionNotSupportedException(Object value, Class<?> requiredType,
+ String name, MethodParameter param, Throwable cause) {
+
+ super(value, requiredType, cause);
+ this.name = name;
+ this.parameter = param;
+ }
+
+
+ /**
+ * Return the name of the method argument.
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Return the target method parameter.
+ */
+ public MethodParameter getParameter() {
+ return this.parameter;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentTypeMismatchException.java b/spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentTypeMismatchException.java
new file mode 100644
index 00000000..14da6517
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/MethodArgumentTypeMismatchException.java
@@ -0,0 +1,61 @@
+/*
+ * 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.web.method.annotation;
+
+import org.springframework.beans.TypeMismatchException;
+import org.springframework.core.MethodParameter;
+
+/**
+ * A TypeMismatchException raised while resolving a controller method argument.
+ * Provides access to the target {@link org.springframework.core.MethodParameter
+ * MethodParameter}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+@SuppressWarnings("serial")
+public class MethodArgumentTypeMismatchException extends TypeMismatchException {
+
+ private final String name;
+
+ private final MethodParameter parameter;
+
+
+ public MethodArgumentTypeMismatchException(Object value, Class<?> requiredType,
+ String name, MethodParameter param, Throwable cause) {
+
+ super(value, requiredType, cause);
+ this.name = name;
+ this.parameter = param;
+ }
+
+
+ /**
+ * Return the name of the method argument.
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Return the target method parameter.
+ */
+ public MethodParameter getParameter() {
+ return this.parameter;
+ }
+
+}
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 9ee2bbde..9eb31c0d 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
@@ -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.
@@ -45,14 +45,14 @@ import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
- * Provides methods to initialize the {@link Model} before controller method
- * invocation and to update it afterwards.
+ * Assist with initialization of the {@link Model} before controller method
+ * invocation and with updates to it after the invocation.
*
- * <p>On initialization, the model is populated with attributes from the session
- * and by invoking methods annotated with {@code @ModelAttribute}.
+ * <p>On initialization the model is populated with attributes temporarily stored
+ * in the session and through the invocation of {@code @ModelAttribute} methods.
*
- * <p>On update, model attributes are synchronized with the session and also
- * {@link BindingResult} attributes are added where missing.
+ * <p>On update model attributes are synchronized with the session and also
+ * {@link BindingResult} attributes are added if missing.
*
* @author Rossen Stoyanchev
* @since 3.1
@@ -70,51 +70,51 @@ public final class ModelFactory {
/**
* Create a new instance with the given {@code @ModelAttribute} methods.
- * @param invocableMethods the {@code @ModelAttribute} methods to invoke
- * @param dataBinderFactory for preparation of {@link BindingResult} attributes
- * @param sessionAttributesHandler for access to session attributes
+ * @param handlerMethods the {@code @ModelAttribute} methods to invoke
+ * @param binderFactory for preparation of {@link BindingResult} attributes
+ * @param attributeHandler for access to session attributes
*/
- public ModelFactory(List<InvocableHandlerMethod> invocableMethods, WebDataBinderFactory dataBinderFactory,
- SessionAttributesHandler sessionAttributesHandler) {
+ public ModelFactory(List<InvocableHandlerMethod> handlerMethods,
+ WebDataBinderFactory binderFactory, SessionAttributesHandler attributeHandler) {
- if (invocableMethods != null) {
- for (InvocableHandlerMethod method : invocableMethods) {
- this.modelMethods.add(new ModelMethod(method));
+ if (handlerMethods != null) {
+ for (InvocableHandlerMethod handlerMethod : handlerMethods) {
+ this.modelMethods.add(new ModelMethod(handlerMethod));
}
}
- this.dataBinderFactory = dataBinderFactory;
- this.sessionAttributesHandler = sessionAttributesHandler;
+ this.dataBinderFactory = binderFactory;
+ this.sessionAttributesHandler = attributeHandler;
}
+
/**
* Populate the model in the following order:
* <ol>
- * <li>Retrieve "known" session attributes listed as {@code @SessionAttributes}.
- * <li>Invoke {@code @ModelAttribute} methods
- * <li>Find {@code @ModelAttribute} method arguments also listed as
- * {@code @SessionAttributes} and ensure they're present in the model raising
- * an exception if necessary.
+ * <li>Retrieve "known" session attributes listed as {@code @SessionAttributes}.
+ * <li>Invoke {@code @ModelAttribute} methods
+ * <li>Find {@code @ModelAttribute} method arguments also listed as
+ * {@code @SessionAttributes} and ensure they're present in the model raising
+ * an exception if necessary.
* </ol>
* @param request the current request
- * @param mavContainer a container with the model to be initialized
+ * @param container a container with the model to be initialized
* @param handlerMethod the method for which the model is initialized
* @throws Exception may arise from {@code @ModelAttribute} methods
*/
- public void initModel(NativeWebRequest request, ModelAndViewContainer mavContainer, HandlerMethod handlerMethod)
- throws Exception {
+ public void initModel(NativeWebRequest request, ModelAndViewContainer container,
+ HandlerMethod handlerMethod) throws Exception {
Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
- mavContainer.mergeAttributes(sessionAttributes);
-
- invokeModelAttributeMethods(request, mavContainer);
+ container.mergeAttributes(sessionAttributes);
+ invokeModelAttributeMethods(request, container);
for (String name : findSessionAttributeArguments(handlerMethod)) {
- if (!mavContainer.containsAttribute(name)) {
+ if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
}
- mavContainer.addAttribute(name, value);
+ container.addAttribute(name, value);
}
}
}
@@ -123,30 +123,29 @@ public final class ModelFactory {
* Invoke model attribute methods to populate the model.
* Attributes are added only if not already present in the model.
*/
- private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer mavContainer)
+ private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container)
throws Exception {
while (!this.modelMethods.isEmpty()) {
- InvocableHandlerMethod attrMethod = getNextModelMethod(mavContainer).getHandlerMethod();
- String modelName = attrMethod.getMethodAnnotation(ModelAttribute.class).value();
- if (mavContainer.containsAttribute(modelName)) {
+ InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
+ String modelName = modelMethod.getMethodAnnotation(ModelAttribute.class).value();
+ if (container.containsAttribute(modelName)) {
continue;
}
- Object returnValue = attrMethod.invokeForRequest(request, mavContainer);
-
- if (!attrMethod.isVoid()){
- String returnValueName = getNameForReturnValue(returnValue, attrMethod.getReturnType());
- if (!mavContainer.containsAttribute(returnValueName)) {
- mavContainer.addAttribute(returnValueName, returnValue);
+ Object returnValue = modelMethod.invokeForRequest(request, container);
+ if (!modelMethod.isVoid()){
+ String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
+ if (!container.containsAttribute(returnValueName)) {
+ container.addAttribute(returnValueName, returnValue);
}
}
}
}
- private ModelMethod getNextModelMethod(ModelAndViewContainer mavContainer) {
+ private ModelMethod getNextModelMethod(ModelAndViewContainer container) {
for (ModelMethod modelMethod : this.modelMethods) {
- if (modelMethod.checkDependencies(mavContainer)) {
+ if (modelMethod.checkDependencies(container)) {
if (logger.isTraceEnabled()) {
logger.trace("Selected @ModelAttribute method " + modelMethod);
}
@@ -157,7 +156,7 @@ public final class ModelFactory {
ModelMethod modelMethod = this.modelMethods.get(0);
if (logger.isTraceEnabled()) {
logger.trace("Selected @ModelAttribute method (not present: " +
- modelMethod.getUnresolvedDependencies(mavContainer)+ ") " + modelMethod);
+ modelMethod.getUnresolvedDependencies(container)+ ") " + modelMethod);
}
this.modelMethods.remove(modelMethod);
return modelMethod;
@@ -171,7 +170,8 @@ public final class ModelFactory {
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
String name = getNameForParameter(parameter);
- if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, parameter.getParameterType())) {
+ Class<?> paramType = parameter.getParameterType();
+ if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, paramType)) {
result.add(name);
}
}
@@ -182,36 +182,37 @@ public final class ModelFactory {
/**
* Derives the model attribute name for a method parameter based on:
* <ol>
- * <li>The parameter {@code @ModelAttribute} annotation value
- * <li>The parameter type
+ * <li>The parameter {@code @ModelAttribute} annotation value
+ * <li>The parameter type
* </ol>
* @return the derived name; never {@code null} or an empty string
*/
public static String getNameForParameter(MethodParameter parameter) {
- ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
- String attrName = (annot != null) ? annot.value() : null;
- return StringUtils.hasText(attrName) ? attrName : Conventions.getVariableNameForParameter(parameter);
+ ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
+ String name = (ann != null ? ann.value() : null);
+ return StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter);
}
/**
* Derive the model attribute name for the given return value using one of:
* <ol>
- * <li>The method {@code ModelAttribute} annotation value
- * <li>The declared return type if it is more specific than {@code Object}
- * <li>The actual return value type
+ * <li>The method {@code ModelAttribute} annotation value
+ * <li>The declared return type if it is more specific than {@code Object}
+ * <li>The actual return value type
* </ol>
* @param returnValue the value returned from a method invocation
* @param returnType the return type of the method
* @return the model name, never {@code null} nor empty
*/
public static String getNameForReturnValue(Object returnValue, MethodParameter returnType) {
- ModelAttribute annotation = returnType.getMethodAnnotation(ModelAttribute.class);
- if (annotation != null && StringUtils.hasText(annotation.value())) {
- return annotation.value();
+ ModelAttribute ann = returnType.getMethodAnnotation(ModelAttribute.class);
+ if (ann != null && StringUtils.hasText(ann.value())) {
+ return ann.value();
}
else {
Method method = returnType.getMethod();
- Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, returnType.getContainingClass());
+ Class<?> containingClass = returnType.getContainingClass();
+ Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
}
}
@@ -220,18 +221,18 @@ public final class ModelFactory {
* Promote model attributes listed as {@code @SessionAttributes} to the session.
* Add {@link BindingResult} attributes where necessary.
* @param request the current request
- * @param mavContainer contains the model to update
+ * @param container contains the model to update
* @throws Exception if creating BindingResult attributes fails
*/
- public void updateModel(NativeWebRequest request, ModelAndViewContainer mavContainer) throws Exception {
- ModelMap defaultModel = mavContainer.getDefaultModel();
- if (mavContainer.getSessionStatus().isComplete()){
+ public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
+ ModelMap defaultModel = container.getDefaultModel();
+ if (container.getSessionStatus().isComplete()){
this.sessionAttributesHandler.cleanupAttributes(request);
}
else {
this.sessionAttributesHandler.storeAttributes(request, defaultModel);
}
- if (!mavContainer.isRequestHandled() && mavContainer.getModel() == defaultModel) {
+ if (!container.isRequestHandled() && container.getModel() == defaultModel) {
updateBindingResult(request, defaultModel);
}
}
@@ -248,7 +249,7 @@ public final class ModelFactory {
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name;
if (!model.containsAttribute(bindingResultKey)) {
- WebDataBinder dataBinder = dataBinderFactory.createBinder(request, value, name);
+ WebDataBinder dataBinder = this.dataBinderFactory.createBinder(request, value, name);
model.put(bindingResultKey, dataBinder.getBindingResult());
}
}
@@ -279,7 +280,6 @@ public final class ModelFactory {
private final Set<String> dependencies = new HashSet<String>();
-
private ModelMethod(InvocableHandlerMethod handlerMethod) {
this.handlerMethod = handlerMethod;
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java
index 1b8b46fd..f2ad4a42 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.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.
@@ -66,8 +66,11 @@ public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgu
}
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
- for (String headerValue : webRequest.getHeaderValues(headerName)) {
- result.add(headerName, headerValue);
+ String[] headerValues = webRequest.getHeaderValues(headerName);
+ if (headerValues != null) {
+ for (String headerValue : headerValues) {
+ result.add(headerName, headerValue);
+ }
}
}
return result;
@@ -77,7 +80,9 @@ public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgu
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String headerValue = webRequest.getHeader(headerName);
- result.put(headerName, headerValue);
+ if (headerValue != null) {
+ result.put(headerName, headerValue);
+ }
}
return result;
}
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 5ac1cc53..3ef1b4fe 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -55,8 +55,8 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho
@Override
public boolean supportsParameter(MethodParameter parameter) {
- return parameter.hasParameterAnnotation(RequestHeader.class) &&
- !Map.class.isAssignableFrom(parameter.getParameterType());
+ return (parameter.hasParameterAnnotation(RequestHeader.class) &&
+ !Map.class.isAssignableFrom(parameter.getParameterType()));
}
@Override
@@ -86,7 +86,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho
private static class RequestHeaderNamedValueInfo extends NamedValueInfo {
private RequestHeaderNamedValueInfo(RequestHeader annotation) {
- super(annotation.value(), annotation.required(), annotation.defaultValue());
+ super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMapMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMapMethodArgumentResolver.java
index 3038be27..39fce258 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMapMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMapMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -49,10 +49,10 @@ public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgum
@Override
public boolean supportsParameter(MethodParameter parameter) {
- RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
- if (ann != null) {
+ RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
+ if (requestParam != null) {
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
- return !StringUtils.hasText(ann.value());
+ return !StringUtils.hasText(requestParam.name());
}
}
return false;
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 a3284175..aba8999a 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
@@ -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.
@@ -127,7 +127,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
Class<?> paramType = parameter.getParameterType();
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (Map.class.isAssignableFrom(paramType)) {
- String paramName = parameter.getParameterAnnotation(RequestParam.class).value();
+ String paramName = parameter.getParameterAnnotation(RequestParam.class).name();
return StringUtils.hasText(paramName);
}
else {
@@ -138,7 +138,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
- else if (MultipartFile.class.equals(paramType) || "javax.servlet.http.Part".equals(paramType.getName())) {
+ else if (MultipartFile.class == paramType || "javax.servlet.http.Part".equals(paramType.getName())) {
return true;
}
else if (this.useDefaultResolution) {
@@ -163,7 +163,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
Object arg;
- if (MultipartFile.class.equals(parameter.getParameterType())) {
+ if (MultipartFile.class == parameter.getParameterType()) {
assertIsMultipartRequest(servletRequest);
Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
arg = multipartRequest.getFile(name);
@@ -218,11 +218,11 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
}
private boolean isMultipartFileCollection(MethodParameter parameter) {
- return MultipartFile.class.equals(getCollectionParameterType(parameter));
+ return (MultipartFile.class == getCollectionParameterType(parameter));
}
private boolean isMultipartFileArray(MethodParameter parameter) {
- return MultipartFile.class.equals(parameter.getParameterType().getComponentType());
+ return (MultipartFile.class == parameter.getParameterType().getComponentType());
}
private boolean isPartCollection(MethodParameter parameter) {
@@ -237,7 +237,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
private Class<?> getCollectionParameterType(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
- if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){
+ if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){
Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(parameter);
if (valueType != null) {
return valueType;
@@ -256,13 +256,14 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
Class<?> paramType = parameter.getParameterType();
- if (Map.class.isAssignableFrom(paramType) || MultipartFile.class.equals(paramType) ||
+ if (Map.class.isAssignableFrom(paramType) || MultipartFile.class == paramType ||
"javax.servlet.http.Part".equals(paramType.getName())) {
return;
}
- RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
- String name = (ann == null || StringUtils.isEmpty(ann.value()) ? parameter.getParameterName() : ann.value());
+ RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
+ String name = (requestParam == null || StringUtils.isEmpty(requestParam.name()) ?
+ parameter.getParameterName() : requestParam.name());
if (value == null) {
builder.queryParam(name);
@@ -301,7 +302,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod
}
public RequestParamNamedValueInfo(RequestParam annotation) {
- super(annotation.value(), annotation.required(), annotation.defaultValue());
+ super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
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 2430c66d..3ff2b315 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-2013 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.
@@ -70,8 +70,8 @@ public class SessionAttributesHandler {
SessionAttributes annotation = AnnotationUtils.findAnnotation(handlerType, SessionAttributes.class);
if (annotation != null) {
- this.attributeNames.addAll(Arrays.asList(annotation.value()));
- this.attributeTypes.addAll(Arrays.<Class<?>>asList(annotation.types()));
+ this.attributeNames.addAll(Arrays.asList(annotation.names()));
+ this.attributeTypes.addAll(Arrays.asList(annotation.types()));
}
for (String attributeName : this.attributeNames) {
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionStatusMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionStatusMethodArgumentResolver.java
index 1e4dedf7..b6b30f9a 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionStatusMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionStatusMethodArgumentResolver.java
@@ -34,7 +34,7 @@ public class SessionStatusMethodArgumentResolver implements HandlerMethodArgumen
@Override
public boolean supportsParameter(MethodParameter parameter) {
- return SessionStatus.class.equals(parameter.getParameterType());
+ return SessionStatus.class == parameter.getParameterType();
}
@Override
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/AsyncHandlerMethodReturnValueHandler.java b/spring-web/src/main/java/org/springframework/web/method/support/AsyncHandlerMethodReturnValueHandler.java
new file mode 100644
index 00000000..cade2438
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/method/support/AsyncHandlerMethodReturnValueHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.web.method.support;
+
+import org.springframework.core.MethodParameter;
+
+/**
+ * A {@link HandlerMethodReturnValueHandler} that handles return values that
+ * represent asynchronous computation. Such handlers need to be invoked with
+ * precedence over other handlers that might otherwise match the return value
+ * type: e.g. a method that returns a Promise type that is also annotated with
+ * {@code @ResponseBody}.
+ *
+ * <p>In {@link #handleReturnValue}, implementations of this class should create
+ * a {@link org.springframework.web.context.request.async.DeferredResult} or
+ * adapt to it and then invoke {@code WebAsyncManager} to start async processing.
+ * For example:
+ * <pre>
+ * DeferredResult<?> deferredResult = (DeferredResult<?>) returnValue;
+ * WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
+ * </pre>
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface AsyncHandlerMethodReturnValueHandler extends HandlerMethodReturnValueHandler {
+
+ /**
+ * Whether the given return value represents asynchronous computation.
+ * @param returnValue the return value
+ * @param returnType the return type
+ * @return {@code true} if the return value is asynchronous.
+ */
+ boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType);
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/CompositeUriComponentsContributor.java b/spring-web/src/main/java/org/springframework/web/method/support/CompositeUriComponentsContributor.java
index f061d86e..1df794a7 100644
--- a/spring-web/src/main/java/org/springframework/web/method/support/CompositeUriComponentsContributor.java
+++ b/spring-web/src/main/java/org/springframework/web/method/support/CompositeUriComponentsContributor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -117,16 +117,16 @@ public class CompositeUriComponentsContributor implements UriComponentsContribut
public void contributeMethodArgument(MethodParameter parameter, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
- for (Object c : this.contributors) {
- if (c instanceof UriComponentsContributor) {
- UriComponentsContributor contributor = (UriComponentsContributor) c;
- if (contributor.supportsParameter(parameter)) {
- contributor.contributeMethodArgument(parameter, value, builder, uriVariables, conversionService);
+ for (Object contributor : this.contributors) {
+ if (contributor instanceof UriComponentsContributor) {
+ UriComponentsContributor ucc = (UriComponentsContributor) contributor;
+ if (ucc.supportsParameter(parameter)) {
+ ucc.contributeMethodArgument(parameter, value, builder, uriVariables, conversionService);
break;
}
}
- else if (c instanceof HandlerMethodArgumentResolver) {
- if (((HandlerMethodArgumentResolver) c).supportsParameter(parameter)) {
+ else if (contributor instanceof HandlerMethodArgumentResolver) {
+ if (((HandlerMethodArgumentResolver) contributor).supportsParameter(parameter)) {
break;
}
}
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 904af697..5492582e 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
@@ -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.
@@ -48,12 +48,33 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
/**
+ * Add the given {@link HandlerMethodArgumentResolver}.
+ */
+ public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) {
+ this.argumentResolvers.add(resolver);
+ return this;
+ }
+
+ /**
+ * Add the given {@link HandlerMethodArgumentResolver}s.
+ */
+ public HandlerMethodArgumentResolverComposite addResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
+ if (resolvers != null) {
+ for (HandlerMethodArgumentResolver resolver : resolvers) {
+ this.argumentResolvers.add(resolver);
+ }
+ }
+ return this;
+ }
+
+ /**
* Return a read-only list with the contained resolvers, or an empty list.
*/
public List<HandlerMethodArgumentResolver> getResolvers() {
return Collections.unmodifiableList(this.argumentResolvers);
}
+
/**
* Whether the given {@linkplain MethodParameter method parameter} is supported by any registered
* {@link HandlerMethodArgumentResolver}.
@@ -99,24 +120,4 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
return result;
}
- /**
- * Add the given {@link HandlerMethodArgumentResolver}.
- */
- public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) {
- this.argumentResolvers.add(resolver);
- return this;
- }
-
- /**
- * Add the given {@link HandlerMethodArgumentResolver}s.
- */
- public HandlerMethodArgumentResolverComposite addResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
- if (resolvers != null) {
- for (HandlerMethodArgumentResolver resolver : resolvers) {
- this.argumentResolvers.add(resolver);
- }
- }
- return this;
- }
-
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerComposite.java b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerComposite.java
index 5f57fcda..847068de 100644
--- a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerComposite.java
+++ b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerComposite.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,7 +24,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
-import org.springframework.util.Assert;
import org.springframework.web.context.request.NativeWebRequest;
/**
@@ -34,7 +33,7 @@ import org.springframework.web.context.request.NativeWebRequest;
* @author Rossen Stoyanchev
* @since 3.1
*/
-public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
+public class HandlerMethodReturnValueHandlerComposite implements AsyncHandlerMethodReturnValueHandler {
protected final Log logger = LogFactory.getLog(getClass());
@@ -58,6 +57,15 @@ public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodRe
return getReturnValueHandler(returnType) != null;
}
+ private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
+ for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
+ if (handler.supportsReturnType(returnType)) {
+ return handler;
+ }
+ }
+ return null;
+ }
+
/**
* Iterate over registered {@link HandlerMethodReturnValueHandler}s and invoke the one that supports it.
* @throws IllegalStateException if no suitable {@link HandlerMethodReturnValueHandler} is found.
@@ -66,32 +74,43 @@ public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodRe
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
- HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType);
- Assert.notNull(handler, "Unknown return value type [" + returnType.getParameterType().getName() + "]");
+ HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
+ if (handler == null) {
+ throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
+ }
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
- /**
- * Find a registered {@link HandlerMethodReturnValueHandler} that supports the given return type.
- */
- private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
- for (HandlerMethodReturnValueHandler returnValueHandler : returnValueHandlers) {
- if (logger.isTraceEnabled()) {
- logger.trace("Testing if return value handler [" + returnValueHandler + "] supports [" +
- returnType.getGenericParameterType() + "]");
+ private HandlerMethodReturnValueHandler selectHandler(Object value, MethodParameter returnType) {
+ boolean isAsyncValue = isAsyncReturnValue(value, returnType);
+ for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
+ if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
+ continue;
}
- if (returnValueHandler.supportsReturnType(returnType)) {
- return returnValueHandler;
+ if (handler.supportsReturnType(returnType)) {
+ return handler;
}
}
return null;
}
+ @Override
+ public boolean isAsyncReturnValue(Object value, MethodParameter returnType) {
+ for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
+ if (handler instanceof AsyncHandlerMethodReturnValueHandler) {
+ if (((AsyncHandlerMethodReturnValueHandler) handler).isAsyncReturnValue(value, returnType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Add the given {@link HandlerMethodReturnValueHandler}.
*/
public HandlerMethodReturnValueHandlerComposite addHandler(HandlerMethodReturnValueHandler handler) {
- returnValueHandlers.add(handler);
+ this.returnValueHandlers.add(handler);
return this;
}
diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java
index 23972879..3a296369 100644
--- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java
+++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.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,8 +39,7 @@ import org.springframework.web.method.HandlerMethod;
* conversion. Use the {@link #setDataBinderFactory(WebDataBinderFactory)} property to supply
* a binder factory to pass to argument resolvers.
*
- * <p>Use {@link #setHandlerMethodArgumentResolvers(HandlerMethodArgumentResolverComposite)}
- * to customize the list of argument resolvers.
+ * <p>Use {@link #setHandlerMethodArgumentResolvers} to customize the list of argument resolvers.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
@@ -223,7 +222,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
- throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex);
+ String message = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
+ throw new IllegalStateException(getInvocationErrorMessage(message, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
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 d4856075..536d53b0 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
@@ -126,7 +126,10 @@ public class ModelAndViewContainer {
return this.defaultModel;
}
else {
- return (this.redirectModel != null) ? this.redirectModel : new ModelMap();
+ if (this.redirectModel == null) {
+ this.redirectModel = new ModelMap();
+ }
+ return this.redirectModel;
}
}
@@ -144,7 +147,8 @@ public class ModelAndViewContainer {
* model (redirect URL preparation). Use of this method may be needed for
* advanced cases when access to the "default" model is needed regardless,
* e.g. to save model attributes specified via {@code @SessionAttributes}.
- * @return the default model, never {@code null}
+ * @return the default model (never {@code null})
+ * @since 4.1.4
*/
public ModelMap getDefaultModel() {
return this.defaultModel;
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java b/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java
index 4d4a0450..6097bd39 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/MultipartFile.java
@@ -20,6 +20,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import org.springframework.core.io.InputStreamSource;
+
/**
* A representation of an uploaded file received in a multipart request.
*
@@ -34,7 +36,7 @@ import java.io.InputStream;
* @see org.springframework.web.multipart.MultipartHttpServletRequest
* @see org.springframework.web.multipart.MultipartResolver
*/
-public interface MultipartFile {
+public interface MultipartFile extends InputStreamSource {
/**
* Return the name of the parameter in the multipart form.
@@ -84,6 +86,7 @@ public interface MultipartFile {
* @return the contents of the file as stream, or an empty stream if empty
* @throws IOException in case of access errors (if the temporary store fails)
*/
+ @Override
InputStream getInputStream() throws IOException;
/**
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 f6e6f6c6..622b1844 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
@@ -100,7 +100,7 @@ public abstract class CommonsFileUploadSupport {
}
/**
- * Set the maximum allowed size (in bytes) before uploads are refused.
+ * Set the maximum allowed size (in bytes) before an upload gets rejected.
* -1 indicates no limit (the default).
* @param maxUploadSize the maximum upload size allowed
* @see org.apache.commons.fileupload.FileUploadBase#setSizeMax
@@ -110,6 +110,17 @@ public abstract class CommonsFileUploadSupport {
}
/**
+ * Set the maximum allowed size (in bytes) for each individual file before
+ * an upload gets rejected. -1 indicates no limit (the default).
+ * @param maxUploadSizePerFile the maximum upload size per file
+ * @since 4.2
+ * @see org.apache.commons.fileupload.FileUploadBase#setFileSizeMax
+ */
+ public void setMaxUploadSizePerFile(long maxUploadSizePerFile) {
+ this.fileUpload.setFileSizeMax(maxUploadSizePerFile);
+ }
+
+ /**
* Set the maximum allowed size (in bytes) before uploads are written to disk.
* Uploaded files will still be received past this amount, but they will not be
* stored in memory. Default is 10240, according to Commons FileUpload.
@@ -200,6 +211,7 @@ public abstract class CommonsFileUploadSupport {
if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) {
actualFileUpload = newFileUpload(getFileItemFactory());
actualFileUpload.setSizeMax(fileUpload.getSizeMax());
+ actualFileUpload.setFileSizeMax(fileUpload.getFileSizeMax());
actualFileUpload.setHeaderEncoding(encoding);
}
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java
index eb0c3349..ac7d9f11 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java
@@ -16,7 +16,6 @@
package org.springframework.web.multipart.commons;
-import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -28,6 +27,7 @@ import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile;
/**
@@ -125,7 +125,7 @@ public class CommonsMultipartFile implements MultipartFile, Serializable {
throw new IllegalStateException("File has been moved - cannot be read again");
}
InputStream inputStream = this.fileItem.getInputStream();
- return (inputStream != null ? inputStream : new ByteArrayInputStream(new byte[0]));
+ return (inputStream != null ? inputStream : StreamUtils.emptyInput());
}
@Override
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java
index decbe385..4e423e44 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.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.
@@ -61,7 +61,7 @@ public abstract class AbstractMultipartHttpServletRequest extends HttpServletReq
@Override
public HttpMethod getRequestMethod() {
- return HttpMethod.valueOf(getRequest().getMethod());
+ return HttpMethod.resolve(getRequest().getMethod());
}
@Override
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java
index 422517bd..905525b0 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.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.
@@ -24,27 +24,28 @@ import org.springframework.web.multipart.MultipartResolver;
* Raised when the part of a "multipart/form-data" request identified by its
* name cannot be found.
*
- * <p>This may be because the request is not a multipart/form-data
- *
- * either because the part is not present in the request, or
- * because the web application is not configured correctly for processing
- * multipart requests -- e.g. no {@link MultipartResolver}.
+ * <p>This may be because the request is not a multipart/form-data request,
+ * because the part is not present in the request, or because the web
+ * application is not configured correctly for processing multipart requests,
+ * e.g. no {@link MultipartResolver}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
+@SuppressWarnings("serial")
public class MissingServletRequestPartException extends ServletException {
- private static final long serialVersionUID = -1255077391966870705L;
-
private final String partName;
+
public MissingServletRequestPartException(String partName) {
- super("Required request part '" + partName + "' is not present.");
+ super("Required request part '" + partName + "' is not present");
this.partName = partName;
}
+
public String getRequestPartName() {
return this.partName;
}
+
}
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 152f21ef..78eab918 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
@@ -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.
@@ -19,9 +19,11 @@ package org.springframework.web.multipart.support;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.Charset;
import javax.servlet.http.HttpServletRequest;
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;
@@ -29,6 +31,7 @@ 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
@@ -50,8 +53,8 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques
/**
- * Create a new instance.
- * @param request the current request
+ * Create a new {@code RequestPartServletServerHttpRequest} instance.
+ * @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
@@ -79,8 +82,9 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques
}
private static MultipartHttpServletRequest asMultipartRequest(HttpServletRequest request) {
- if (request instanceof MultipartHttpServletRequest) {
- return (MultipartHttpServletRequest) request;
+ MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
+ if (unwrapped != null) {
+ return unwrapped;
}
else if (ClassUtils.hasMethod(HttpServletRequest.class, "getParts")) {
// Servlet 3.0 available ..
@@ -89,11 +93,13 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques
throw new IllegalArgumentException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
}
+
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
+
@Override
public InputStream getBody() throws IOException {
if (this.multipartRequest instanceof StandardMultipartHttpServletRequest) {
@@ -111,9 +117,21 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques
}
else {
String paramValue = this.multipartRequest.getParameter(this.partName);
- return new ByteArrayInputStream(paramValue.getBytes(FORM_CHARSET));
+ return new ByteArrayInputStream(paramValue.getBytes(determineEncoding()));
+ }
+ }
+ }
+
+ private String determineEncoding() {
+ MediaType contentType = getHeaders().getContentType();
+ if (contentType != null) {
+ Charset charset = contentType.getCharSet();
+ if (charset != null) {
+ return charset.name();
}
}
+ String encoding = this.multipartRequest.getCharacterEncoding();
+ return (encoding != null ? encoding : FORM_CHARSET);
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java
index b842d9e9..a86f713e 100644
--- a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardMultipartHttpServletRequest.java
@@ -20,6 +20,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -44,6 +45,7 @@ import org.springframework.web.multipart.MultipartFile;
* methods - without any custom processing on our side.
*
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
* @since 3.1
*/
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
@@ -52,6 +54,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
private static final String FILENAME_KEY = "filename=";
+ private static final String FILENAME_WITH_CHARSET_KEY = "filename*=";
+
+ private static final Charset US_ASCII = Charset.forName("us-ascii");
+
+
private Set<String> multipartParameterNames;
@@ -86,7 +93,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
for (Part part : parts) {
- String filename = extractFilename(part.getHeader(CONTENT_DISPOSITION));
+ String disposition = part.getHeader(CONTENT_DISPOSITION);
+ String filename = extractFilename(disposition);
+ if (filename == null) {
+ filename = extractFilenameWithCharset(disposition);
+ }
if (filename != null) {
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
@@ -102,15 +113,18 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
}
private String extractFilename(String contentDisposition) {
+ return extractFilename(contentDisposition, FILENAME_KEY);
+ }
+
+ private String extractFilename(String contentDisposition, String key) {
if (contentDisposition == null) {
return null;
}
- // TODO: can only handle the typical case at the moment
- int startIndex = contentDisposition.indexOf(FILENAME_KEY);
+ int startIndex = contentDisposition.indexOf(key);
if (startIndex == -1) {
return null;
}
- String filename = contentDisposition.substring(startIndex + FILENAME_KEY.length());
+ String filename = contentDisposition.substring(startIndex + key.length());
if (filename.startsWith("\"")) {
int endIndex = filename.indexOf("\"", 1);
if (endIndex != -1) {
@@ -126,6 +140,33 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
return filename;
}
+ private String extractFilenameWithCharset(String contentDisposition) {
+ String filename = extractFilename(contentDisposition, FILENAME_WITH_CHARSET_KEY);
+ if (filename == null) {
+ return null;
+ }
+ int index = filename.indexOf("'");
+ if (index != -1) {
+ Charset charset = null;
+ try {
+ charset = Charset.forName(filename.substring(0, index));
+ }
+ catch (IllegalArgumentException ex) {
+ // ignore
+ }
+ filename = filename.substring(index + 1);
+ // Skip language information..
+ index = filename.indexOf("'");
+ if (index != -1) {
+ filename = filename.substring(index + 1);
+ }
+ if (charset != null) {
+ filename = new String(filename.getBytes(US_ASCII), charset);
+ }
+ }
+ return filename;
+ }
+
@Override
protected void initializeMultipart() {
diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java
index cfca663a..9b9a97cf 100644
--- a/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java
+++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingRequestWrapper.java
@@ -30,6 +30,8 @@ import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
+import org.springframework.http.HttpMethod;
+
/**
* {@link javax.servlet.http.HttpServletRequest} wrapper that caches all content read from
* the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader},
@@ -46,8 +48,6 @@ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
- private static final String METHOD_POST = "POST";
-
private final ByteArrayOutputStream cachedContent;
@@ -125,7 +125,7 @@ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private boolean isFormPost() {
String contentType = getContentType();
return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
- METHOD_POST.equalsIgnoreCase(getMethod()));
+ HttpMethod.POST.matches(getMethod()));
}
private void writeRequestParametersToCachedContent() {
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 ff358bea..7bc23949 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
@@ -17,15 +17,16 @@
package org.springframework.web.util;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
+
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
-import org.springframework.util.ResizableByteArrayOutputStream;
-import org.springframework.util.StreamUtils;
+import org.springframework.util.FastByteArrayOutputStream;
/**
* {@link javax.servlet.http.HttpServletResponse} wrapper that caches all content written to
@@ -40,7 +41,7 @@ import org.springframework.util.StreamUtils;
*/
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
- private final ResizableByteArrayOutputStream content = new ResizableByteArrayOutputStream(1024);
+ private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream();
@@ -75,7 +76,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public void sendError(int sc) throws IOException {
- copyBodyToResponse();
+ copyBodyToResponse(false);
try {
super.sendError(sc);
}
@@ -89,7 +90,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
@SuppressWarnings("deprecation")
public void sendError(int sc, String msg) throws IOException {
- copyBodyToResponse();
+ copyBodyToResponse(false);
try {
super.sendError(sc, msg);
}
@@ -102,12 +103,12 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public void sendRedirect(String location) throws IOException {
- copyBodyToResponse();
+ copyBodyToResponse(false);
super.sendRedirect(location);
}
@Override
- public ServletOutputStream getOutputStream() {
+ public ServletOutputStream getOutputStream() throws IOException {
return this.outputStream;
}
@@ -122,8 +123,13 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
}
@Override
+ public void flushBuffer() throws IOException {
+ // do not flush the underlying response as the content as not been copied to it yet
+ }
+
+ @Override
public void setContentLength(int len) {
- if (len > this.content.capacity()) {
+ if (len > this.content.size()) {
this.content.resize(len);
}
this.contentLength = len;
@@ -136,7 +142,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
Integer.MAX_VALUE + "): " + len);
}
int lenInt = (int) len;
- if (lenInt > this.content.capacity()) {
+ if (lenInt > this.content.size()) {
this.content.resize(lenInt);
}
this.contentLength = lenInt;
@@ -144,7 +150,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public void setBufferSize(int size) {
- if (size > this.content.capacity()) {
+ if (size > this.content.size()) {
this.content.resize(size);
}
}
@@ -174,14 +180,48 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
return this.content.toByteArray();
}
- private void copyBodyToResponse() throws IOException {
+ /**
+ * Return an {@link InputStream} to the cached content.
+ * @since 4.2
+ */
+ public InputStream getContentInputStream() {
+ return this.content.getInputStream();
+ }
+
+ /**
+ * Return the current size of the cached content.
+ * @since 4.2
+ */
+ public int getContentSize() {
+ return this.content.size();
+ }
+
+ /**
+ * Copy the complete cached body content to the response.
+ * @since 4.2
+ */
+ public void copyBodyToResponse() throws IOException {
+ copyBodyToResponse(true);
+ }
+
+ /**
+ * Copy the cached body content to the response.
+ * @param complete whether to set a corresponding content length
+ * for the complete cached body content
+ * @since 4.2
+ */
+ protected void copyBodyToResponse(boolean complete) throws IOException {
if (this.content.size() > 0) {
- if (this.contentLength != null) {
- getResponse().setContentLength(this.contentLength);
+ HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
+ if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
+ rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
this.contentLength = null;
}
- StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream());
+ this.content.writeTo(rawResponse.getOutputStream());
this.content.reset();
+ if (complete) {
+ super.flushBuffer();
+ }
}
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java b/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java
index c79a59e6..9e3ba5c0 100644
--- a/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java
+++ b/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 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.
@@ -46,13 +46,6 @@ public class CookieGenerator {
*/
public static final String DEFAULT_COOKIE_PATH = "/";
- /**
- * Default maximum age of cookies: maximum integer value, i.e. forever.
- * @deprecated in favor of setting no max age value at all in such a case
- */
- @Deprecated
- public static final int DEFAULT_COOKIE_MAX_AGE = Integer.MAX_VALUE;
-
protected final Log logger = LogFactory.getLog(getClass());
@@ -210,6 +203,12 @@ public class CookieGenerator {
Assert.notNull(response, "HttpServletResponse must not be null");
Cookie cookie = createCookie("");
cookie.setMaxAge(0);
+ if (isCookieSecure()) {
+ cookie.setSecure(true);
+ }
+ if (isCookieHttpOnly()) {
+ cookie.setHttpOnly(true);
+ }
response.addCookie(cookie);
if (logger.isDebugEnabled()) {
logger.debug("Removed cookie with name [" + getCookieName() + "]");
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
new file mode 100644
index 00000000..e4f79832
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java
@@ -0,0 +1,130 @@
+/*
+ * 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.web.util;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+
+/**
+ * Default implementation of {@link UriTemplateHandler} that relies on
+ * {@link UriComponentsBuilder} internally.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public class DefaultUriTemplateHandler implements UriTemplateHandler {
+
+ private String baseUrl;
+
+ private boolean parsePath;
+
+
+ /**
+ * 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>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
+ */
+ public void setParsePath(boolean parsePath) {
+ this.parsePath = parsePath;
+ }
+
+ /**
+ * Whether the handler is configured to parse the path into path segments.
+ */
+ public boolean shouldParsePath() {
+ return this.parsePath;
+ }
+
+
+ @Override
+ public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
+ UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
+ UriComponents uriComponents = uriComponentsBuilder.build().expand(uriVariables).encode();
+ return insertBaseUrl(uriComponents);
+ }
+
+ @Override
+ public URI expand(String uriTemplate, Object... uriVariableValues) {
+ UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate);
+ UriComponents uriComponents = uriComponentsBuilder.build().expand(uriVariableValues).encode();
+ return insertBaseUrl(uriComponents);
+ }
+
+ protected UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) {
+ UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate);
+ if (shouldParsePath()) {
+ List<String> pathSegments = builder.build().getPathSegments();
+ builder.replacePath(null);
+ for (String pathSegment : pathSegments) {
+ builder.pathSegment(pathSegment);
+ }
+ }
+ return builder;
+ }
+
+ protected URI insertBaseUrl(UriComponents uriComponents) {
+ if (getBaseUrl() == null || uriComponents.getHost() != null) {
+ return uriComponents.toUri();
+ }
+ String url = getBaseUrl() + uriComponents.toUriString();
+ try {
+ return new URI(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/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java
index 3e880e75..e25466ec 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
@@ -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.
@@ -39,6 +39,7 @@ import org.springframework.util.StringUtils;
* Extension of {@link UriComponents} for hierarchical URIs.
*
* @author Arjen Poutsma
+ * @author Rossen Stoyanchev
* @author Phillip Webb
* @since 3.1.3
* @see <a href="http://tools.ietf.org/html/rfc3986#section-1.2.3">Hierarchical URIs</a>
@@ -60,7 +61,6 @@ 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
@@ -73,8 +73,9 @@ final class HierarchicalUriComponents extends UriComponents {
* @param encoded whether the components are already encoded
* @param verify whether the components need to be checked for illegal characters
*/
- HierarchicalUriComponents(String scheme, String userInfo, String host, String port, PathComponent path,
- MultiValueMap<String, String> queryParams, String fragment, boolean encoded, boolean verify) {
+ HierarchicalUriComponents(String scheme, String userInfo, String host, String port,
+ PathComponent path, MultiValueMap<String, String> queryParams,
+ String fragment, boolean encoded, boolean verify) {
super(scheme, fragment);
this.userInfo = userInfo;
@@ -175,41 +176,44 @@ final class HierarchicalUriComponents extends UriComponents {
// encoding
/**
- * Encodes all URI components using their specific encoding rules, and returns the result as a new
- * {@code UriComponents} instance.
+ * Encode all URI components using their specific encoding rules and return
+ * the result as a new {@code UriComponents} instance.
* @param encoding the encoding of the values contained in this map
* @return the encoded uri components
* @throws UnsupportedEncodingException if the given encoding is not supported
*/
@Override
public HierarchicalUriComponents encode(String encoding) throws UnsupportedEncodingException {
- Assert.hasLength(encoding, "Encoding must not be empty");
if (this.encoded) {
return this;
}
- String encodedScheme = encodeUriComponent(getScheme(), encoding, Type.SCHEME);
- String encodedUserInfo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO);
- String encodedHost = encodeUriComponent(this.host, encoding, getHostType());
+ Assert.hasLength(encoding, "Encoding must not be empty");
+ String schemeTo = encodeUriComponent(getScheme(), encoding, Type.SCHEME);
+ String userInfoTo = encodeUriComponent(this.userInfo, encoding, Type.USER_INFO);
+ String hostTo = encodeUriComponent(this.host, encoding, getHostType());
+ PathComponent pathTo = this.path.encode(encoding);
+ MultiValueMap<String, String> paramsTo = encodeQueryParams(encoding);
+ String fragmentTo = encodeUriComponent(this.getFragment(), encoding, Type.FRAGMENT);
+ return new HierarchicalUriComponents(schemeTo, userInfoTo, hostTo, this.port,
+ pathTo, paramsTo, fragmentTo, true, false);
+ }
- PathComponent encodedPath = this.path.encode(encoding);
- MultiValueMap<String, String> encodedQueryParams =
- new LinkedMultiValueMap<String, String>(this.queryParams.size());
+ private MultiValueMap<String, String> encodeQueryParams(String encoding) throws UnsupportedEncodingException {
+ int size = this.queryParams.size();
+ MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(size);
for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) {
- String encodedName = encodeUriComponent(entry.getKey(), encoding, Type.QUERY_PARAM);
- List<String> encodedValues = new ArrayList<String>(entry.getValue().size());
+ String name = encodeUriComponent(entry.getKey(), encoding, Type.QUERY_PARAM);
+ List<String> values = new ArrayList<String>(entry.getValue().size());
for (String value : entry.getValue()) {
- String encodedValue = encodeUriComponent(value, encoding, Type.QUERY_PARAM);
- encodedValues.add(encodedValue);
+ values.add(encodeUriComponent(value, encoding, Type.QUERY_PARAM));
}
- encodedQueryParams.put(encodedName, encodedValues);
+ result.put(name, values);
}
- String encodedFragment = encodeUriComponent(this.getFragment(), encoding, Type.FRAGMENT);
- return new HierarchicalUriComponents(encodedScheme, encodedUserInfo, encodedHost, this.port, encodedPath,
- encodedQueryParams, encodedFragment, true, false);
+ return result;
}
/**
- * Encodes the given source into an encoded String using the rules specified
+ * Encode the given source into an encoded String using the rules specified
* by the given component and with the given options.
* @param source the source string
* @param encoding the encoding of the source string
@@ -217,7 +221,9 @@ final class HierarchicalUriComponents extends UriComponents {
* @return the encoded URI
* @throws IllegalArgumentException when the given uri parameter is not a valid URI
*/
- static String encodeUriComponent(String source, String encoding, Type type) throws UnsupportedEncodingException {
+ static String encodeUriComponent(String source, String encoding, Type type)
+ throws UnsupportedEncodingException {
+
if (source == null) {
return null;
}
@@ -282,7 +288,7 @@ final class HierarchicalUriComponents extends UriComponents {
return;
}
int length = source.length();
- for (int i=0; i < length; i++) {
+ for (int i = 0; i < length; i++) {
char ch = source.charAt(i);
if (ch == '%') {
if ((i + 2) < length) {
@@ -291,17 +297,19 @@ final class HierarchicalUriComponents extends UriComponents {
int u = Character.digit(hex1, 16);
int l = Character.digit(hex2, 16);
if (u == -1 || l == -1) {
- throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
+ throw new IllegalArgumentException("Invalid encoded sequence \"" +
+ source.substring(i) + "\"");
}
i += 2;
}
else {
- throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
+ throw new IllegalArgumentException("Invalid encoded sequence \"" +
+ source.substring(i) + "\"");
}
}
else if (!type.isAllowed(ch)) {
- throw new IllegalArgumentException(
- "Invalid character '" + ch + "' for " + type.name() + " in \"" + source + "\"");
+ throw new IllegalArgumentException("Invalid character '" + ch + "' for " +
+ type.name() + " in \"" + source + "\"");
}
}
}
@@ -312,25 +320,31 @@ final class HierarchicalUriComponents extends UriComponents {
@Override
protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) {
Assert.state(!this.encoded, "Cannot expand an already encoded UriComponents object");
- String expandedScheme = expandUriComponent(getScheme(), uriVariables);
- String expandedUserInfo = expandUriComponent(this.userInfo, uriVariables);
- String expandedHost = expandUriComponent(this.host, uriVariables);
- String expandedPort = expandUriComponent(this.port, uriVariables);
- PathComponent expandedPath = this.path.expand(uriVariables);
- MultiValueMap<String, String> expandedQueryParams =
- new LinkedMultiValueMap<String, String>(this.queryParams.size());
+
+ String schemeTo = expandUriComponent(getScheme(), uriVariables);
+ String userInfoTo = expandUriComponent(this.userInfo, uriVariables);
+ String hostTo = expandUriComponent(this.host, uriVariables);
+ String portTo = expandUriComponent(this.port, uriVariables);
+ PathComponent pathTo = this.path.expand(uriVariables);
+ MultiValueMap<String, String> paramsTo = expandQueryParams(uriVariables);
+ String fragmentTo = expandUriComponent(this.getFragment(), uriVariables);
+
+ return new HierarchicalUriComponents(schemeTo, userInfoTo, hostTo, portTo,
+ pathTo, paramsTo, fragmentTo, false, false);
+ }
+
+ private MultiValueMap<String, String> expandQueryParams(UriTemplateVariables variables) {
+ int size = this.queryParams.size();
+ MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(size);
for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) {
- String expandedName = expandUriComponent(entry.getKey(), uriVariables);
- List<String> expandedValues = new ArrayList<String>(entry.getValue().size());
+ String name = expandUriComponent(entry.getKey(), variables);
+ List<String> values = new ArrayList<String>(entry.getValue().size());
for (String value : entry.getValue()) {
- String expandedValue = expandUriComponent(value, uriVariables);
- expandedValues.add(expandedValue);
+ values.add(expandUriComponent(value, variables));
}
- expandedQueryParams.put(expandedName, expandedValues);
+ result.put(name, values);
}
- String expandedFragment = expandUriComponent(this.getFragment(), uriVariables);
- return new HierarchicalUriComponents(expandedScheme, expandedUserInfo, expandedHost, expandedPort, expandedPath,
- expandedQueryParams, expandedFragment, false, false);
+ return result;
}
/**
@@ -418,6 +432,19 @@ final class HierarchicalUriComponents extends UriComponents {
}
@Override
+ protected void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
+ builder.scheme(getScheme());
+ builder.userInfo(getUserInfo());
+ builder.host(getHost());
+ builder.port(getPort());
+ builder.replacePath("");
+ this.path.copyToUriComponentsBuilder(builder);
+ builder.replaceQueryParams(getQueryParams());
+ builder.fragment(getFragment());
+ }
+
+
+ @Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
@@ -451,11 +478,11 @@ final class HierarchicalUriComponents extends UriComponents {
// inner types
/**
- * Enumeration used to identify the parts of a URI.
+ * Enumeration used to identify the allowed characters per URI component.
* <p>Contains methods to indicate whether a given character is valid in a specific URI component.
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>
*/
- static enum Type {
+ enum Type {
SCHEME {
@Override
@@ -527,6 +554,12 @@ final class HierarchicalUriComponents extends UriComponents {
public boolean isAllowed(int c) {
return isPchar(c) || '/' == c || '?' == c;
}
+ },
+ URI {
+ @Override
+ public boolean isAllowed(int c) {
+ return isUnreserved(c);
+ }
};
/**
@@ -572,8 +605,8 @@ final class HierarchicalUriComponents extends UriComponents {
* Indicates whether the given character is in the {@code reserved} set.
* @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a>
*/
- protected boolean isReserved(char c) {
- return isGenericDelimiter(c) || isReserved(c);
+ protected boolean isReserved(int c) {
+ return isGenericDelimiter(c) || isSubDelimiter(c);
}
/**
@@ -608,6 +641,8 @@ final class HierarchicalUriComponents extends UriComponents {
void verify();
PathComponent expand(UriTemplateVariables uriVariables);
+
+ void copyToUriComponentsBuilder(UriComponentsBuilder builder);
}
@@ -618,6 +653,7 @@ final class HierarchicalUriComponents extends UriComponents {
private final String path;
+
public FullPathComponent(String path) {
this.path = path;
}
@@ -637,8 +673,7 @@ final class HierarchicalUriComponents extends UriComponents {
@Override
public PathComponent encode(String encoding) throws UnsupportedEncodingException {
String encodedPath = encodeUriComponent(getPath(),encoding, Type.PATH);
- return new FullPathComponent(encodedPath);
- }
+ return new FullPathComponent(encodedPath); }
@Override
public void verify() {
@@ -652,6 +687,11 @@ final class HierarchicalUriComponents extends UriComponents {
}
@Override
+ public void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
+ builder.path(getPath());
+ }
+
+ @Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof FullPathComponent &&
getPath().equals(((FullPathComponent) obj).getPath())));
@@ -672,6 +712,7 @@ final class HierarchicalUriComponents extends UriComponents {
private final List<String> pathSegments;
public PathSegmentComponent(List<String> pathSegments) {
+ Assert.notNull(pathSegments);
this.pathSegments = Collections.unmodifiableList(new ArrayList<String>(pathSegments));
}
@@ -724,6 +765,11 @@ final class HierarchicalUriComponents extends UriComponents {
}
@Override
+ public void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
+ builder.pathSegment(getPathSegments().toArray(new String[getPathSegments().size()]));
+ }
+
+ @Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof PathSegmentComponent &&
getPathSegments().equals(((PathSegmentComponent) obj).getPathSegments())));
@@ -744,6 +790,7 @@ final class HierarchicalUriComponents extends UriComponents {
private final List<PathComponent> pathComponents;
public PathComponentComposite(List<PathComponent> pathComponents) {
+ Assert.notNull(pathComponents);
this.pathComponents = pathComponents;
}
@@ -789,6 +836,13 @@ final class HierarchicalUriComponents extends UriComponents {
}
return new PathComponentComposite(expandedComponents);
}
+
+ @Override
+ public void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
+ for (PathComponent pathComponent : this.pathComponents) {
+ pathComponent.copyToUriComponentsBuilder(builder);
+ }
+ }
}
@@ -816,6 +870,9 @@ final class HierarchicalUriComponents extends UriComponents {
return this;
}
@Override
+ public void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
+ }
+ @Override
public boolean equals(Object obj) {
return (this == obj);
}
diff --git a/spring-web/src/main/java/org/springframework/web/util/Log4jConfigListener.java b/spring-web/src/main/java/org/springframework/web/util/Log4jConfigListener.java
index ad38baa0..a6ab9a57 100644
--- a/spring-web/src/main/java/org/springframework/web/util/Log4jConfigListener.java
+++ b/spring-web/src/main/java/org/springframework/web/util/Log4jConfigListener.java
@@ -38,7 +38,10 @@ import javax.servlet.ServletContextListener;
* @see Log4jWebConfigurer
* @see org.springframework.web.context.ContextLoaderListener
* @see WebAppRootListener
+ * @deprecated as of Spring 4.2.1, in favor of Apache Log4j 2
+ * (following Apache's EOL declaration for log4j 1.x)
*/
+@Deprecated
public class Log4jConfigListener implements ServletContextListener {
@Override
diff --git a/spring-web/src/main/java/org/springframework/web/util/Log4jWebConfigurer.java b/spring-web/src/main/java/org/springframework/web/util/Log4jWebConfigurer.java
index 5fe7a6ae..c1588f17 100644
--- a/spring-web/src/main/java/org/springframework/web/util/Log4jWebConfigurer.java
+++ b/spring-web/src/main/java/org/springframework/web/util/Log4jWebConfigurer.java
@@ -19,7 +19,6 @@ package org.springframework.web.util;
import java.io.FileNotFoundException;
import javax.servlet.ServletContext;
-import org.springframework.util.Log4jConfigurer;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
@@ -93,7 +92,10 @@ import org.springframework.util.StringUtils;
* @since 12.08.2003
* @see org.springframework.util.Log4jConfigurer
* @see Log4jConfigListener
+ * @deprecated as of Spring 4.2.1, in favor of Apache Log4j 2
+ * (following Apache's EOL declaration for log4j 1.x)
*/
+@Deprecated
public abstract class Log4jWebConfigurer {
/** Parameter specifying the location of the log4j config file */
@@ -141,7 +143,7 @@ public abstract class Log4jWebConfigurer {
// checking the file in the background.
try {
long refreshInterval = Long.parseLong(intervalString);
- Log4jConfigurer.initLogging(location, refreshInterval);
+ org.springframework.util.Log4jConfigurer.initLogging(location, refreshInterval);
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid 'log4jRefreshInterval' parameter: " + ex.getMessage());
@@ -149,7 +151,7 @@ public abstract class Log4jWebConfigurer {
}
else {
// Initialize without refresh check, i.e. without log4j's watchdog thread.
- Log4jConfigurer.initLogging(location);
+ org.springframework.util.Log4jConfigurer.initLogging(location);
}
}
catch (FileNotFoundException ex) {
@@ -167,7 +169,7 @@ public abstract class Log4jWebConfigurer {
public static void shutdownLogging(ServletContext servletContext) {
servletContext.log("Shutting down log4j");
try {
- Log4jConfigurer.shutdownLogging();
+ org.springframework.util.Log4jConfigurer.shutdownLogging();
}
finally {
// Remove the web app root system property.
diff --git a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
index 438ec0b1..e71f1b30 100644
--- a/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
+++ b/spring-web/src/main/java/org/springframework/web/util/OpaqueUriComponents.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -135,6 +135,13 @@ final class OpaqueUriComponents extends UriComponents {
}
}
+ @Override
+ protected void copyToUriComponentsBuilder(UriComponentsBuilder builder) {
+ builder.scheme(getScheme());
+ builder.schemeSpecificPart(getSchemeSpecificPart());
+ builder.fragment(getFragment());
+ }
+
@Override
public boolean equals(Object obj) {
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java
index fc747ae9..b9d2c607 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 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.
@@ -203,6 +203,12 @@ public abstract class UriComponents implements Serializable {
return toUriString();
}
+ /**
+ * Set all components of the given UriComponentsBuilder.
+ * @since 4.2
+ */
+ protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder);
+
// static expansion helpers
@@ -213,6 +219,9 @@ public abstract class UriComponents implements Serializable {
if (source.indexOf('{') == -1) {
return source;
}
+ if (source.indexOf(':') != -1) {
+ source = sanitizeSource(source);
+ }
Matcher matcher = NAMES_PATTERN.matcher(source);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
@@ -230,6 +239,27 @@ public abstract class UriComponents implements Serializable {
return sb.toString();
}
+ /**
+ * Remove nested "{}" such as in URI vars with regular expressions.
+ */
+ private static String sanitizeSource(String source) {
+ int level = 0;
+ StringBuilder sb = new StringBuilder();
+ for (char c : source.toCharArray()) {
+ if (c == '{') {
+ level++;
+ }
+ if (c == '}') {
+ level--;
+ }
+ if (level > 1 || (level == 1 && c == '}')) {
+ continue;
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
private static String getVariableName(String match) {
int colonIdx = match.indexOf(':');
return (colonIdx != -1 ? match.substring(0, colonIdx) : match);
@@ -246,7 +276,7 @@ public abstract class UriComponents implements Serializable {
*/
public interface UriTemplateVariables {
- public static final Object SKIP_VALUE = UriTemplateVariables.class;
+ Object SKIP_VALUE = UriTemplateVariables.class;
/**
* Get the value for the given URI variable name.
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
index 0b4fa360..31c0162e 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
@@ -88,6 +88,10 @@ public class UriComponentsBuilder implements Cloneable {
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?");
+ private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
+
+ private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
+
private String scheme;
@@ -179,7 +183,7 @@ public class UriComponentsBuilder implements Cloneable {
* @return the new {@code UriComponentsBuilder}
*/
public static UriComponentsBuilder fromUriString(String uri) {
- Assert.hasLength(uri, "'uri' must not be empty");
+ Assert.notNull(uri, "URI must not be null");
Matcher matcher = URI_PATTERN.matcher(uri);
if (matcher.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
@@ -239,7 +243,7 @@ public class UriComponentsBuilder implements Cloneable {
* @return the URI components of the URI
*/
public static UriComponentsBuilder fromHttpUrl(String httpUrl) {
- Assert.notNull(httpUrl, "'httpUrl' must not be null");
+ Assert.notNull(httpUrl, "HTTP URL must not be null");
Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
if (matcher.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
@@ -267,12 +271,14 @@ public class UriComponentsBuilder implements Cloneable {
/**
* Create a new {@code UriComponents} object from the URI associated with
* the given HttpRequest while also overlaying with values from the headers
- * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if present.
+ * "Forwarded" (<a href="http://tools.ietf.org/html/rfc7239">RFC 7239</a>, or
+ * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if "Forwarded" is
+ * not found.
* @param request the source request
* @return the URI components of the URI
* @since 4.1.5
*/
- public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {
+ public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {
URI uri = request.getURI();
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(uri);
@@ -280,31 +286,45 @@ public class UriComponentsBuilder implements Cloneable {
String host = uri.getHost();
int port = uri.getPort();
- String hostHeader = request.getHeaders().getFirst("X-Forwarded-Host");
- if (StringUtils.hasText(hostHeader)) {
- String[] hosts = StringUtils.commaDelimitedListToStringArray(hostHeader);
- String hostToUse = hosts[0];
- if (hostToUse.contains(":")) {
- String[] hostAndPort = StringUtils.split(hostToUse, ":");
- host = hostAndPort[0];
- port = Integer.parseInt(hostAndPort[1]);
+ String forwardedHeader = request.getHeaders().getFirst("Forwarded");
+ if (StringUtils.hasText(forwardedHeader)) {
+ String forwardedToUse = StringUtils.commaDelimitedListToStringArray(forwardedHeader)[0];
+ Matcher m = FORWARDED_HOST_PATTERN.matcher(forwardedToUse);
+ if (m.find()) {
+ host = m.group(1).trim();
}
- else {
- host = hostToUse;
- port = -1;
+ m = FORWARDED_PROTO_PATTERN.matcher(forwardedToUse);
+ if (m.find()) {
+ scheme = m.group(1).trim();
}
}
+ else {
+ String hostHeader = request.getHeaders().getFirst("X-Forwarded-Host");
+ if (StringUtils.hasText(hostHeader)) {
+ String[] hosts = StringUtils.commaDelimitedListToStringArray(hostHeader);
+ String hostToUse = hosts[0];
+ if (hostToUse.contains(":")) {
+ String[] hostAndPort = StringUtils.split(hostToUse, ":");
+ host = hostAndPort[0];
+ port = Integer.parseInt(hostAndPort[1]);
+ }
+ else {
+ host = hostToUse;
+ port = -1;
+ }
+ }
- String portHeader = request.getHeaders().getFirst("X-Forwarded-Port");
- if (StringUtils.hasText(portHeader)) {
- String[] ports = StringUtils.commaDelimitedListToStringArray(portHeader);
- port = Integer.parseInt(ports[0]);
- }
+ String portHeader = request.getHeaders().getFirst("X-Forwarded-Port");
+ if (StringUtils.hasText(portHeader)) {
+ String[] ports = StringUtils.commaDelimitedListToStringArray(portHeader);
+ port = Integer.parseInt(ports[0]);
+ }
- String protocolHeader = request.getHeaders().getFirst("X-Forwarded-Proto");
- if (StringUtils.hasText(protocolHeader)) {
- String[] protocols = StringUtils.commaDelimitedListToStringArray(protocolHeader);
- scheme = protocols[0];
+ String protocolHeader = request.getHeaders().getFirst("X-Forwarded-Proto");
+ if (StringUtils.hasText(protocolHeader)) {
+ String[] protocols = StringUtils.commaDelimitedListToStringArray(protocolHeader);
+ scheme = protocols[0];
+ }
}
builder.scheme(scheme);
@@ -316,28 +336,33 @@ public class UriComponentsBuilder implements Cloneable {
return builder;
}
+
/**
- * Create an instance by parsing the "origin" header of an HTTP request.
+ * Create an instance by parsing the "Origin" header of an HTTP request.
+ * @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454</a>
*/
public static UriComponentsBuilder fromOriginHeader(String origin) {
- UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
- if (StringUtils.hasText(origin)) {
- int schemaIdx = origin.indexOf("://");
- String schema = (schemaIdx != -1 ? origin.substring(0, schemaIdx) : "http");
- builder.scheme(schema);
- String hostString = (schemaIdx != -1 ? origin.substring(schemaIdx + 3) : origin);
- if (hostString.contains(":")) {
- String[] hostAndPort = StringUtils.split(hostString, ":");
- builder.host(hostAndPort[0]);
- builder.port(Integer.parseInt(hostAndPort[1]));
+ Matcher matcher = URI_PATTERN.matcher(origin);
+ if (matcher.matches()) {
+ UriComponentsBuilder builder = new UriComponentsBuilder();
+ String scheme = matcher.group(2);
+ String host = matcher.group(6);
+ String port = matcher.group(8);
+ if (StringUtils.hasLength(scheme)) {
+ builder.scheme(scheme);
}
- else {
- builder.host(hostString);
+ builder.host(host);
+ if (StringUtils.hasLength(port)) {
+ builder.port(port);
}
+ return builder;
+ }
+ else {
+ throw new IllegalArgumentException("[" + origin + "] is not a valid \"Origin\" header value");
}
- return builder;
}
+
// build methods
/**
@@ -407,7 +432,7 @@ public class UriComponentsBuilder implements Cloneable {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder uri(URI uri) {
- Assert.notNull(uri, "'uri' must not be null");
+ Assert.notNull(uri, "URI must not be null");
this.scheme = uri.getScheme();
if (uri.isOpaque()) {
this.ssp = uri.getRawSchemeSpecificPart();
@@ -467,41 +492,8 @@ public class UriComponentsBuilder implements Cloneable {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
- Assert.notNull(uriComponents, "'uriComponents' must not be null");
- this.scheme = uriComponents.getScheme();
- if (uriComponents instanceof OpaqueUriComponents) {
- this.ssp = uriComponents.getSchemeSpecificPart();
- resetHierarchicalComponents();
- }
- else {
- if (uriComponents.getUserInfo() != null) {
- this.userInfo = uriComponents.getUserInfo();
- }
- if (uriComponents.getHost() != null) {
- this.host = uriComponents.getHost();
- }
- if (uriComponents.getPort() != -1) {
- this.port = String.valueOf(uriComponents.getPort());
- }
- if (StringUtils.hasLength(uriComponents.getPath())) {
- List<String> segments = uriComponents.getPathSegments();
- if (segments.isEmpty()) {
- // Perhaps "/"
- this.pathBuilder.addPath(uriComponents.getPath());
- }
- else {
- this.pathBuilder.addPathSegments(segments.toArray(new String[segments.size()]));
- }
- }
- if (!uriComponents.getQueryParams().isEmpty()) {
- this.queryParams.clear();
- this.queryParams.putAll(uriComponents.getQueryParams());
- }
- resetSchemeSpecificPart();
- }
- if (uriComponents.getFragment() != null) {
- this.fragment = uriComponents.getFragment();
- }
+ Assert.notNull(uriComponents, "UriComponents must not be null");
+ uriComponents.copyToUriComponentsBuilder(this);
return this;
}
@@ -549,7 +541,7 @@ public class UriComponentsBuilder implements Cloneable {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder port(int port) {
- Assert.isTrue(port >= -1, "'port' must not be < -1");
+ Assert.isTrue(port >= -1, "Port must be >= -1");
this.port = String.valueOf(port);
resetSchemeSpecificPart();
return this;
@@ -592,13 +584,13 @@ public class UriComponentsBuilder implements Cloneable {
}
/**
- * Append the given path segments to the existing path of this builder.
- * Each given path segment may contain URI template variables.
+ * Append path segments to the existing path. Each path segment may contain
+ * URI template variables and should not contain any slashes.
+ * Use {@code path("/")} subsequently to ensure a trailing slash.
* @param pathSegments the URI path segments
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
- Assert.notNull(pathSegments, "'segments' must not be null");
this.pathBuilder.addPathSegments(pathSegments);
resetSchemeSpecificPart();
return this;
@@ -613,8 +605,9 @@ public class UriComponentsBuilder implements Cloneable {
* be parsed unambiguously. Such values should be substituted for URI
* variables to enable correct parsing:
* <pre class="code">
- * String uriString = &quot;/hotels/42?filter={value}&quot;;
- * UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
+ * UriComponentsBuilder.fromUriString(&quot;/hotels/42&quot;)
+ * .query(&quot;filter={value}&quot;)
+ * .buildAndExpand(&quot;hot&amp;cold&quot;);
* </pre>
* @param query the query string
* @return this UriComponentsBuilder
@@ -658,7 +651,7 @@ public class UriComponentsBuilder implements Cloneable {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder queryParam(String name, Object... values) {
- Assert.notNull(name, "'name' must not be null");
+ Assert.notNull(name, "Name must not be null");
if (!ObjectUtils.isEmpty(values)) {
for (Object value : values) {
String valueAsString = (value != null ? value.toString() : null);
@@ -678,8 +671,9 @@ public class UriComponentsBuilder implements Cloneable {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder queryParams(MultiValueMap<String, String> params) {
- Assert.notNull(params, "'params' must not be null");
- this.queryParams.putAll(params);
+ if (params != null) {
+ this.queryParams.putAll(params);
+ }
return this;
}
@@ -691,7 +685,7 @@ public class UriComponentsBuilder implements Cloneable {
* @return this UriComponentsBuilder
*/
public UriComponentsBuilder replaceQueryParam(String name, Object... values) {
- Assert.notNull(name, "'name' must not be null");
+ Assert.notNull(name, "Name must not be null");
this.queryParams.remove(name);
if (!ObjectUtils.isEmpty(values)) {
queryParam(name, values);
@@ -701,6 +695,19 @@ public class UriComponentsBuilder implements Cloneable {
}
/**
+ * Set the query parameter values overriding all existing query values.
+ * @param params the query parameter name
+ * @return this UriComponentsBuilder
+ */
+ public UriComponentsBuilder replaceQueryParams(MultiValueMap<String, String> params) {
+ this.queryParams.clear();
+ if (params != null) {
+ this.queryParams.putAll(params);
+ }
+ return this;
+ }
+
+ /**
* Set the URI fragment. The given fragment may contain URI template variables,
* and may also be {@code null} to clear the fragment of this builder.
* @param fragment the URI fragment
@@ -708,7 +715,7 @@ public class UriComponentsBuilder implements Cloneable {
*/
public UriComponentsBuilder fragment(String fragment) {
if (fragment != null) {
- Assert.hasLength(fragment, "'fragment' must not be empty");
+ Assert.hasLength(fragment, "Fragment must not be empty");
this.fragment = fragment;
}
else {
@@ -718,7 +725,7 @@ public class UriComponentsBuilder implements Cloneable {
}
@Override
- protected Object clone() {
+ public Object clone() {
return new UriComponentsBuilder(this);
}
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 17d98f7b..43d60943 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
@@ -18,9 +18,9 @@ package org.springframework.web.util;
import java.io.Serializable;
import java.net.URI;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@@ -37,19 +37,13 @@ import org.springframework.util.Assert;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
* @since 3.0
* @see <a href="http://bitworking.org/projects/URI-Templates/">URI Templates</a>
*/
@SuppressWarnings("serial")
public class UriTemplate implements Serializable {
- /** Captures URI template variable names. */
- private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
-
- /** Replaces template variables in the URI template. */
- private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
-
-
private final UriComponents uriComponents;
private final List<String> variableNames;
@@ -64,11 +58,13 @@ public class UriTemplate implements Serializable {
* @param uriTemplate the URI template string
*/
public UriTemplate(String uriTemplate) {
- Parser parser = new Parser(uriTemplate);
+ Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
this.uriTemplate = uriTemplate;
- this.variableNames = parser.getVariableNames();
- this.matchPattern = parser.getMatchPattern();
this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build();
+
+ TemplateInfo info = TemplateInfo.parse(uriTemplate);
+ this.variableNames = Collections.unmodifiableList(info.getVariableNames());
+ this.matchPattern = info.getMatchPattern();
}
@@ -169,60 +165,80 @@ public class UriTemplate implements Serializable {
/**
- * Static inner class to parse URI template strings into a matching regular expression.
+ * Helper to extract variable names and regex for matching to actual URLs.
*/
- private static class Parser {
-
- private final List<String> variableNames = new LinkedList<String>();
-
- private final StringBuilder patternBuilder = new StringBuilder();
-
- private Parser(String uriTemplate) {
- Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
- Matcher matcher = NAMES_PATTERN.matcher(uriTemplate);
- int end = 0;
- while (matcher.find()) {
- this.patternBuilder.append(quote(uriTemplate, end, matcher.start()));
- String match = matcher.group(1);
- int colonIdx = match.indexOf(':');
- if (colonIdx == -1) {
- this.patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
- this.variableNames.add(match);
+ private static class TemplateInfo {
+
+ private final List<String> variableNames;
+
+ private final Pattern pattern;
+
+
+ private TemplateInfo(List<String> vars, Pattern pattern) {
+ this.variableNames = vars;
+ this.pattern = pattern;
+ }
+
+ public List<String> getVariableNames() {
+ return this.variableNames;
+ }
+
+ public Pattern getMatchPattern() {
+ return this.pattern;
+ }
+
+ private static TemplateInfo parse(String uriTemplate) {
+ int level = 0;
+ List<String> variableNames = new ArrayList<String>();
+ StringBuilder pattern = new StringBuilder();
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0 ; i < uriTemplate.length(); i++) {
+ char c = uriTemplate.charAt(i);
+ if (c == '{') {
+ level++;
+ if (level == 1) {
+ // start of URI variable
+ pattern.append(quote(builder));
+ builder = new StringBuilder();
+ continue;
+ }
}
- else {
- if (colonIdx + 1 == match.length()) {
- throw new IllegalArgumentException(
- "No custom regular expression specified after ':' in \"" + match + "\"");
+ else if (c == '}') {
+ level--;
+ if (level == 0) {
+ // end of URI variable
+ String variable = builder.toString();
+ int idx = variable.indexOf(':');
+ if (idx == -1) {
+ pattern.append("(.*)");
+ variableNames.add(variable);
+ }
+ else {
+ if (idx + 1 == variable.length()) {
+ throw new IllegalArgumentException(
+ "No custom regular expression specified after ':' " +
+ "in \"" + variable + "\"");
+ }
+ String regex = variable.substring(idx + 1, variable.length());
+ pattern.append('(');
+ pattern.append(regex);
+ pattern.append(')');
+ variableNames.add(variable.substring(0, idx));
+ }
+ builder = new StringBuilder();
+ continue;
}
- String variablePattern = match.substring(colonIdx + 1, match.length());
- this.patternBuilder.append('(');
- this.patternBuilder.append(variablePattern);
- this.patternBuilder.append(')');
- String variableName = match.substring(0, colonIdx);
- this.variableNames.add(variableName);
}
- end = matcher.end();
+ builder.append(c);
}
- this.patternBuilder.append(quote(uriTemplate, end, uriTemplate.length()));
- int lastIdx = this.patternBuilder.length() - 1;
- if (lastIdx >= 0 && this.patternBuilder.charAt(lastIdx) == '/') {
- this.patternBuilder.deleteCharAt(lastIdx);
+ if (builder.length() > 0) {
+ pattern.append(quote(builder));
}
+ return new TemplateInfo(variableNames, Pattern.compile(pattern.toString()));
}
- private String quote(String fullPath, int start, int end) {
- if (start == end) {
- return "";
- }
- return Pattern.quote(fullPath.substring(start, end));
- }
-
- private List<String> getVariableNames() {
- return Collections.unmodifiableList(this.variableNames);
- }
-
- private Pattern getMatchPattern() {
- return Pattern.compile(this.patternBuilder.toString());
+ private static String quote(StringBuilder builder) {
+ return builder.length() != 0 ? Pattern.quote(builder.toString()) : "";
}
}
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
new file mode 100644
index 00000000..ac04e31e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java
@@ -0,0 +1,46 @@
+/*
+ * 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.web.util;
+
+import java.net.URI;
+import java.util.Map;
+
+/**
+ * A strategy for expanding a URI template with URI variables into a {@link URI}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface UriTemplateHandler {
+
+ /**
+ * Expand the give URI template with a map of URI variables.
+ * @param uriTemplate the URI template string
+ * @param uriVariables the URI variables
+ * @return the resulting URI
+ */
+ URI expand(String uriTemplate, Map<String, ?> uriVariables);
+
+ /**
+ * Expand the give URI template with an array of URI variable values.
+ * @param uriTemplate the URI template string
+ * @param uriVariableValues the URI variable values
+ * @return the resulting URI
+ */
+ URI expand(String uriTemplate, Object... uriVariableValues);
+
+}
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 538e6d30..c6809297 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-2014 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.
@@ -18,8 +18,6 @@ package org.springframework.web.util;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.springframework.util.Assert;
@@ -27,11 +25,11 @@ import org.springframework.util.Assert;
* Utility class for URI encoding and decoding based on RFC 3986.
* Offers encoding methods for the various URI components.
*
- * <p>All {@code encode*(String, String} methods in this class operate in a similar way:
+ * <p>All {@code encode*(String, String)} methods in this class operate in a similar way:
* <ul>
* <li>Valid characters for the specific URI component as defined in RFC 3986 stay the same.</li>
* <li>All other characters are converted into one or more bytes in the given encoding scheme.
- * Each of the resulting bytes is written as a hexadecimal string in the "{@code %<i>xy</i>}"
+ * Each of the resulting bytes is written as a hexadecimal string in the "<code>%<i>xy</i></code>"
* format.</li>
* </ul>
*
@@ -41,174 +39,6 @@ import org.springframework.util.Assert;
*/
public abstract class UriUtils {
- private static final String SCHEME_PATTERN = "([^:/?#]+):";
-
- private static final String HTTP_PATTERN = "(http|https):";
-
- private static final String USERINFO_PATTERN = "([^@/]*)";
-
- private static final String HOST_PATTERN = "([^/?#:]*)";
-
- private static final String PORT_PATTERN = "(\\d*)";
-
- private static final String PATH_PATTERN = "([^?#]*)";
-
- private static final String QUERY_PATTERN = "([^#]*)";
-
- private static final String LAST_PATTERN = "(.*)";
-
- // Regex patterns that matches URIs. See RFC 3986, appendix B
- private static final Pattern URI_PATTERN = Pattern.compile(
- "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
- ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
-
- private static final Pattern HTTP_URL_PATTERN = Pattern.compile(
- "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
- PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?");
-
-
- // encoding
-
- /**
- * Encodes the given source URI into an encoded String. All various URI components are
- * encoded according to their respective valid character sets.
- * <p><strong>Note</strong> that this method does not attempt to encode "=" and "&"
- * characters in query parameter names and query parameter values because they cannot
- * be parsed in a reliable way. Instead use:
- * <pre class="code">
- * UriComponents uriComponents = UriComponentsBuilder.fromUri("/path?name={value}").buildAndExpand("a=b");
- * String encodedUri = uriComponents.encode().toUriString();
- * </pre>
- * @param uri the URI to be encoded
- * @param encoding the character encoding to encode to
- * @return the encoded URI
- * @throws IllegalArgumentException when the given uri parameter is not a valid URI
- * @throws UnsupportedEncodingException when the given encoding parameter is not supported
- * @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding
- */
- @Deprecated
- public static String encodeUri(String uri, String encoding) throws UnsupportedEncodingException {
- Assert.notNull(uri, "URI must not be null");
- Assert.hasLength(encoding, "Encoding must not be empty");
- Matcher matcher = URI_PATTERN.matcher(uri);
- if (matcher.matches()) {
- String scheme = matcher.group(2);
- String authority = matcher.group(3);
- String userinfo = matcher.group(5);
- String host = matcher.group(6);
- String port = matcher.group(8);
- String path = matcher.group(9);
- String query = matcher.group(11);
- String fragment = matcher.group(13);
- return encodeUriComponents(scheme, authority, userinfo, host, port, path, query, fragment, encoding);
- }
- else {
- throw new IllegalArgumentException("[" + uri + "] is not a valid URI");
- }
- }
-
- /**
- * Encodes the given HTTP URI into an encoded String. All various URI components are
- * encoded according to their respective valid character sets.
- * <p><strong>Note</strong> that this method does not support fragments ({@code #}),
- * as these are not supposed to be sent to the server, but retained by the client.
- * <p><strong>Note</strong> that this method does not attempt to encode "=" and "&"
- * characters in query parameter names and query parameter values because they cannot
- * be parsed in a reliable way. Instead use:
- * <pre class="code">
- * UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl("/path?name={value}").buildAndExpand("a=b");
- * String encodedUri = uriComponents.encode().toUriString();
- * </pre>
- * @param httpUrl the HTTP URL to be encoded
- * @param encoding the character encoding to encode to
- * @return the encoded URL
- * @throws IllegalArgumentException when the given uri parameter is not a valid URI
- * @throws UnsupportedEncodingException when the given encoding parameter is not supported
- * @deprecated in favor of {@link UriComponentsBuilder}; see note about query param encoding
- */
- @Deprecated
- public static String encodeHttpUrl(String httpUrl, String encoding) throws UnsupportedEncodingException {
- Assert.notNull(httpUrl, "HTTP URL must not be null");
- Assert.hasLength(encoding, "Encoding must not be empty");
- Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
- if (matcher.matches()) {
- String scheme = matcher.group(1);
- String authority = matcher.group(2);
- String userinfo = matcher.group(4);
- String host = matcher.group(5);
- String portString = matcher.group(7);
- String path = matcher.group(8);
- String query = matcher.group(10);
- return encodeUriComponents(scheme, authority, userinfo, host, portString, path, query, null, encoding);
- }
- else {
- throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
- }
- }
-
- /**
- * Encodes the given source URI components into an encoded String. All various URI components
- * are optional, but encoded according to their respective valid character sets.
- * @param scheme the scheme
- * @param authority the authority
- * @param userInfo the user info
- * @param host the host
- * @param port the port
- * @param path the path
- * @param query the query
- * @param fragment the fragment
- * @param encoding the character encoding to encode to
- * @return the encoded URI
- * @throws IllegalArgumentException when the given uri parameter is not a valid URI
- * @throws UnsupportedEncodingException when the given encoding parameter is not supported
- * @deprecated in favor of {@link UriComponentsBuilder}
- */
- @Deprecated
- public static String encodeUriComponents(String scheme, String authority, String userInfo,
- String host, String port, String path, String query, String fragment, String encoding)
- throws UnsupportedEncodingException {
-
- Assert.hasLength(encoding, "Encoding must not be empty");
- StringBuilder sb = new StringBuilder();
-
- if (scheme != null) {
- sb.append(encodeScheme(scheme, encoding));
- sb.append(':');
- }
-
- if (authority != null) {
- sb.append("//");
- if (userInfo != null) {
- sb.append(encodeUserInfo(userInfo, encoding));
- sb.append('@');
- }
- if (host != null) {
- sb.append(encodeHost(host, encoding));
- }
- if (port != null) {
- sb.append(':');
- sb.append(encodePort(port, encoding));
- }
- }
-
- sb.append(encodePath(path, encoding));
-
- if (query != null) {
- sb.append('?');
- sb.append(encodeQuery(query, encoding));
- }
-
- if (fragment != null) {
- sb.append('#');
- sb.append(encodeFragment(fragment, encoding));
- }
-
- return sb.toString();
- }
-
-
- // encoding convenience methods
-
/**
* Encodes the given URI scheme with the given encoding.
* @param scheme the scheme to be encoded
@@ -319,6 +149,20 @@ public abstract class UriUtils {
return HierarchicalUriComponents.encodeUriComponent(fragment, encoding, HierarchicalUriComponents.Type.FRAGMENT);
}
+ /**
+ * Encode characters outside the unreserved character set as defined in
+ * <a href="https://tools.ietf.org/html/rfc3986#section-2">RFC 3986 Section 2</a>.
+ * <p>This can be used to ensure the given String will not contain any
+ * characters with reserved URI meaning regardless of URI component.
+ * @param source the string to be encoded
+ * @param encoding the character encoding to encode to
+ * @return the encoded string
+ * @throws UnsupportedEncodingException when the given encoding parameter is not supported
+ */
+ public static String encode(String source, String encoding) throws UnsupportedEncodingException {
+ HierarchicalUriComponents.Type type = HierarchicalUriComponents.Type.URI;
+ return HierarchicalUriComponents.encodeUriComponent(source, encoding, type);
+ }
// decoding
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 63cd8693..1a7a4fe3 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-2014 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.
@@ -165,6 +165,8 @@ public class UrlPathHelper {
* i.e. the part of the request's URL beyond the part that called the servlet,
* or "" if the whole URL has been used to identify the servlet.
* <p>Detects include request URL if called within a RequestDispatcher include.
+ * <p>E.g.: servlet mapping = "/*"; request URI = "/test/a" -> "/test/a".
+ * <p>E.g.: servlet mapping = "/"; request URI = "/test/a" -> "/test/a".
* <p>E.g.: servlet mapping = "/test/*"; request URI = "/test/a" -> "/a".
* <p>E.g.: servlet mapping = "/test"; request URI = "/test" -> "".
* <p>E.g.: servlet mapping = "/*.test"; request URI = "/a.test" -> "".
@@ -174,7 +176,17 @@ public class UrlPathHelper {
public String getPathWithinServletMapping(HttpServletRequest request) {
String pathWithinApp = getPathWithinApplication(request);
String servletPath = getServletPath(request);
- String path = getRemainingPath(pathWithinApp, servletPath, false);
+ String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp);
+ String path;
+
+ // if the app container sanitized the servletPath, check against the sanitized version
+ if (servletPath.indexOf(sanitizedPathWithinApp) != -1) {
+ path = getRemainingPath(sanitizedPathWithinApp, servletPath, false);
+ }
+ else {
+ path = getRemainingPath(pathWithinApp, servletPath, false);
+ }
+
if (path != null) {
// Normal case: URI contains servlet path.
return path;
@@ -243,7 +255,7 @@ public class UrlPathHelper {
if (c1 == c2) {
continue;
}
- if (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2))) {
+ else if (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2))) {
continue;
}
return null;
@@ -251,7 +263,7 @@ public class UrlPathHelper {
if (index2 != mapping.length()) {
return null;
}
- if (index1 == requestUri.length()) {
+ else if (index1 == requestUri.length()) {
return "";
}
else if (requestUri.charAt(index1) == ';') {
@@ -261,6 +273,26 @@ public class UrlPathHelper {
}
/**
+ * Sanitize the given path with the following rules:
+ * <ul>
+ * <li>replace all "//" by "/"</li>
+ * </ul>
+ */
+ private String getSanitizedPath(final String path) {
+ String sanitized = path;
+ while (true) {
+ int index = sanitized.indexOf("//");
+ if (index < 0) {
+ break;
+ }
+ else {
+ sanitized = sanitized.substring(0, index) + sanitized.substring(index + 1);
+ }
+ }
+ return sanitized;
+ }
+
+ /**
* Return the request URI for the given request, detecting an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by {@code request.getRequestURI()} is <i>not</i>
@@ -389,6 +421,7 @@ public class UrlPathHelper {
private String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = removeSemicolonContent(uri);
uri = decodeRequestString(request, uri);
+ uri = getSanitizedPath(uri);
return uri;
}
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 778630b4..76c5dd04 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
@@ -33,9 +33,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
import org.springframework.http.HttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -135,14 +132,12 @@ public abstract class WebUtils {
/** Key for the mutex session attribute */
public static final String SESSION_MUTEX_ATTRIBUTE = WebUtils.class.getName() + ".MUTEX";
- private static final Log logger = LogFactory.getLog(WebUtils.class);
-
/**
* Set a system property to the web application root directory.
* The key of the system property can be defined with the "webAppRootKey"
* context-param in {@code web.xml}. Default is "webapp.root".
- * <p>Can be used for tools that support substition with {@code System.getProperty}
+ * <p>Can be used for tools that support substitution with {@code System.getProperty}
* values, like log4j's "${key}" syntax within log file locations.
* @param servletContext the servlet context of the web application
* @throws IllegalStateException if the system property is already set,
@@ -797,15 +792,30 @@ public abstract class WebUtils {
return true;
}
else if (CollectionUtils.isEmpty(allowedOrigins)) {
- UriComponents actualUrl = UriComponentsBuilder.fromHttpRequest(request).build();
- UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
- return (actualUrl.getHost().equals(originUrl.getHost()) && getPort(actualUrl) == getPort(originUrl));
+ return isSameOrigin(request);
}
else {
return allowedOrigins.contains(origin);
}
}
+ /**
+ * Check if the request is a same-origin one, based on {@code Origin}, {@code Host},
+ * {@code Forwarded} and {@code X-Forwarded-Host} headers.
+ * @return {@code true} if the request is a same-origin one, {@code false} in case
+ * of cross-origin request.
+ * @since 4.2
+ */
+ public static boolean isSameOrigin(HttpRequest request) {
+ String origin = request.getHeaders().getOrigin();
+ if (origin == null) {
+ return true;
+ }
+ UriComponents actualUrl = UriComponentsBuilder.fromHttpRequest(request).build();
+ UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
+ return (actualUrl.getHost().equals(originUrl.getHost()) && getPort(actualUrl) == getPort(originUrl));
+ }
+
private static int getPort(UriComponents component) {
int port = component.getPort();
if (port == -1) {