diff options
author | Emmanuel Bourg <ebourg@apache.org> | 2016-08-02 11:13:32 +0200 |
---|---|---|
committer | Emmanuel Bourg <ebourg@apache.org> | 2016-08-02 11:13:32 +0200 |
commit | f69f2a4b8ea697b3a631c0dc7a470e3c9793fee3 (patch) | |
tree | db2f25b29aa3e59c463ab41d3f2856f6265bb1a5 /spring-web/src/main/java/org | |
parent | 5575b60c30c5a0c308c4ba3a2db93956d8c1746c (diff) |
Imported Upstream version 4.2.6
Diffstat (limited to 'spring-web/src/main/java/org')
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; * </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; * </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<String, String>} or + * <p>Supported for annotated handler methods in Servlet and Portlet environments. + * + * <p>If the method parameter is {@link java.util.Map Map<String, String>}, * {@link org.springframework.util.MultiValueMap MultiValueMap<String, String>}, * 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 — + * such as {@code @RequestMapping} and {@code @SessionAttributes} — 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("http://example.com/{foo}", "bar").accept(MediaType.APPLICATION_JSON).body(body); + * RequestEntity request = RequestEntity.post(new URI("http://example.com/foo")).accept(MediaType.APPLICATION_JSON).body(body); * ResponseEntity<MyResponse> 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("http://example.com/{foo}", "bar").accept(MediaType.APPLICATION_JSON).body(body); + * RequestEntity request = RequestEntity.post(new URI("http://example.com/foo")).accept(MediaType.APPLICATION_JSON).body(body); * ParameterizedTypeReference<List<MyResponse>> myBean = new ParameterizedTypeReference<List<MyResponse>>() {}; * ResponseEntity<List<MyResponse>> 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/**/*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/**/*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 = "/hotels/42?filter={value}"; - * UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold"); + * UriComponentsBuilder.fromUriString("/hotels/42") + * .query("filter={value}") + * .buildAndExpand("hot&cold"); * </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) { |