diff options
author | Emmanuel Bourg <ebourg@apache.org> | 2016-08-03 19:55:01 +0200 |
---|---|---|
committer | Emmanuel Bourg <ebourg@apache.org> | 2016-08-03 19:55:01 +0200 |
commit | 75a721d1019da2a2fa86e24ff439df4a224e5b19 (patch) | |
tree | 2c44c00ce2c8641cccad177177e5682e187a17ea /spring-web/src/main | |
parent | 9eaca6a06af3cbceb3754de19d477be770614265 (diff) |
Imported Upstream version 4.3.2
Diffstat (limited to 'spring-web/src/main')
126 files changed, 4893 insertions, 1096 deletions
diff --git a/spring-web/src/main/java/org/springframework/http/CacheControl.java b/spring-web/src/main/java/org/springframework/http/CacheControl.java index a291e7ae..c9933344 100644 --- a/spring-web/src/main/java/org/springframework/http/CacheControl.java +++ b/spring-web/src/main/java/org/springframework/http/CacheControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,10 @@ public class CacheControl { private boolean proxyRevalidate = false; + private long staleWhileRevalidate = -1; + + private long staleIfError = -1; + private long sMaxAge = -1; @@ -110,7 +114,7 @@ public class CacheControl { * clients sending conditional requests (with "ETag", "If-Modified-Since" headers) and the server responding * with "304 - Not Modified" status. * <p>In order to disable caching and minimize requests/responses exchanges, the {@link #noStore()} directive - * should be used. + * should be used instead of {@link #noCache()}. * @return {@code this}, to facilitate method chaining * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a> */ @@ -207,6 +211,36 @@ public class CacheControl { return this; } + /** + * Add a "stale-while-revalidate" directive. + * <p>This directive indicates that caches MAY serve the response in + * which it appears after it becomes stale, up to the indicated number of seconds. + * If a cached response is served stale due to the presence of this extension, + * the cache SHOULD attempt to revalidate it while still serving stale responses (i.e., without blocking). + * @param staleWhileRevalidate the maximum time the response should be used while being revalidated + * @param unit the time unit of the {@code staleWhileRevalidate} argument + * @return {@code this}, to facilitate method chaining + * @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a> + */ + public CacheControl staleWhileRevalidate(long staleWhileRevalidate, TimeUnit unit) { + this.staleWhileRevalidate = unit.toSeconds(staleWhileRevalidate); + return this; + } + + /** + * Add a "stale-if-error" directive. + * <p>This directive indicates that when an error is encountered, a cached stale response MAY be used to satisfy + * the request, regardless of other freshness information. + * @param staleIfError the maximum time the response should be used when errors are encountered + * @param unit the time unit of the {@code staleIfError} argument + * @return {@code this}, to facilitate method chaining + * @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a> + */ + public CacheControl staleIfError(long staleIfError, TimeUnit unit) { + this.staleIfError = unit.toSeconds(staleIfError); + return this; + } + /** * Return the "Cache-Control" header value. @@ -241,6 +275,13 @@ public class CacheControl { if (this.sMaxAge != -1) { appendDirective(ccValue, "s-maxage=" + Long.toString(this.sMaxAge)); } + if (this.staleIfError != -1) { + appendDirective(ccValue, "stale-if-error=" + Long.toString(this.staleIfError)); + } + if (this.staleWhileRevalidate != -1) { + appendDirective(ccValue, "stale-while-revalidate=" + Long.toString(this.staleWhileRevalidate)); + } + String ccHeaderValue = ccValue.toString(); return (StringUtils.hasText(ccHeaderValue) ? ccHeaderValue : null); } diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 1170bb06..0ba9a02a 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -34,6 +34,8 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -55,6 +57,8 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel + * @author Juergen Hoeller * @since 3.0 */ public class HttpHeaders implements MultiValueMap<String, String>, Serializable { @@ -372,6 +376,12 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable "EEE MMM dd HH:mm:ss yyyy" }; + /** + * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match" + * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> + */ + private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); @@ -419,19 +429,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * <p>Returns an empty list when the acceptable media types are unspecified. */ public List<MediaType> getAccept() { - String value = getFirst(ACCEPT); - List<MediaType> result = (value != null ? MediaType.parseMediaTypes(value) : Collections.<MediaType>emptyList()); - - // Some containers parse 'Accept' into multiple values - if (result.size() == 1) { - List<String> acceptHeader = get(ACCEPT); - if (acceptHeader.size() > 1) { - value = StringUtils.collectionToCommaDelimitedString(acceptHeader); - result = MediaType.parseMediaTypes(value); - } - } - - return result; + return MediaType.parseMediaTypes(get(ACCEPT)); } /** @@ -442,10 +440,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Allow-Credentials} response header. + * Return the value of the {@code Access-Control-Allow-Credentials} response header. */ public boolean getAccessControlAllowCredentials() { - return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); + return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); } /** @@ -456,10 +454,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Allow-Headers} response header. + * Return the value of the {@code Access-Control-Allow-Headers} response header. */ public List<String> getAccessControlAllowHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); + return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS); } /** @@ -476,7 +474,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable List<HttpMethod> result = new ArrayList<HttpMethod>(); String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); if (value != null) { - String[] tokens = value.split(",\\s*"); + String[] tokens = StringUtils.tokenizeToStringArray(value, ",", true, true); for (String token : tokens) { HttpMethod resolved = HttpMethod.resolve(token); if (resolved != null) { @@ -498,7 +496,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * Return the value of the {@code Access-Control-Allow-Origin} response header. */ public String getAccessControlAllowOrigin() { - return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); + return getFieldValues(ACCESS_CONTROL_ALLOW_ORIGIN); } /** @@ -509,10 +507,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Expose-Headers} response header. + * Return the value of the {@code Access-Control-Expose-Headers} response header. */ public List<String> getAccessControlExposeHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); + return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS); } /** @@ -523,7 +521,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Max-Age} response header. + * Return the value of the {@code Access-Control-Max-Age} response header. * <p>Returns -1 when the max age is unknown. */ public long getAccessControlMaxAge() { @@ -539,10 +537,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Request-Headers} request header. + * Return the value of the {@code Access-Control-Request-Headers} request header. */ public List<String> getAccessControlRequestHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); + return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS); } /** @@ -643,7 +641,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * Return the value of the {@code Cache-Control} header. */ public String getCacheControl() { - return getFirst(CACHE_CONTROL); + return getFieldValues(CACHE_CONTROL); } /** @@ -664,7 +662,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * Return the value of the {@code Connection} header. */ public List<String> getConnection() { - return getFirstValueAsList(CONNECTION); + return getValuesAsList(CONNECTION); } /** @@ -783,6 +781,30 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** + * Set the (new) value of the {@code If-Match} header. + * @since 4.3 + */ + public void setIfMatch(String ifMatch) { + set(IF_MATCH, ifMatch); + } + + /** + * Set the (new) value of the {@code If-Match} header. + * @since 4.3 + */ + public void setIfMatch(List<String> ifMatchList) { + set(IF_MATCH, toCommaDelimitedString(ifMatchList)); + } + + /** + * Return the value of the {@code If-Match} header. + * @since 4.3 + */ + public List<String> getIfMatch() { + return getETagValuesAsList(IF_MATCH); + } + + /** * Set the (new) value of the {@code If-Modified-Since} header. * <p>The date should be specified as the number of milliseconds since * January 1, 1970 GMT. @@ -814,35 +836,31 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); } - protected String toCommaDelimitedString(List<String> list) { - StringBuilder builder = new StringBuilder(); - for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) { - String ifNoneMatch = iterator.next(); - builder.append(ifNoneMatch); - if (iterator.hasNext()) { - builder.append(", "); - } - } - return builder.toString(); - } - /** * Return the value of the {@code If-None-Match} header. */ public List<String> getIfNoneMatch() { - return getFirstValueAsList(IF_NONE_MATCH); + return getETagValuesAsList(IF_NONE_MATCH); } - protected List<String> getFirstValueAsList(String header) { - List<String> result = new ArrayList<String>(); - String value = getFirst(header); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(token); - } - } - return result; + /** + * Set the (new) value of the {@code If-Unmodified-Since} header. + * <p>The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + * @since 4.3 + */ + public void setIfUnmodifiedSince(long ifUnmodifiedSince) { + setDate(IF_UNMODIFIED_SINCE, ifUnmodifiedSince); + } + + /** + * Return the value of the {@code If-Unmodified-Since} header. + * <p>The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + * @since 4.3 + */ + public long getIfUnmodifiedSince() { + return getFirstDate(IF_UNMODIFIED_SINCE, false); } /** @@ -943,11 +961,43 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** + * Set the request header names (e.g. "Accept-Language") for which the + * response is subject to content negotiation and variances based on the + * value of those request headers. + * @param requestHeaders the request header names + * @since 4.3 + */ + public void setVary(List<String> requestHeaders) { + set(VARY, toCommaDelimitedString(requestHeaders)); + } + + /** + * Return the request header names subject to content negotiation. + * @since 4.3 + */ + public List<String> getVary() { + return getValuesAsList(VARY); + } + + /** + * Set the given date under the given header name after formatting it as a string + * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of + * {@link #set(String, String)} but for date headers. + * @since 3.2.4 + */ + public void setDate(String headerName, long date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); + dateFormat.setTimeZone(GMT); + set(headerName, dateFormat.format(new Date(date))); + } + + /** * Parse the first header value for the given header name as a date, * return -1 if there is no value, or raise {@link IllegalArgumentException} * if the value cannot be parsed as a date. * @param headerName the header name * @return the parsed date header, or -1 if none + * @since 3.2.4 */ public long getFirstDate(String headerName) { return getFirstDate(headerName, true); @@ -992,16 +1042,92 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Set the given date under the given header name after formatting it as a string - * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of - * {@link #set(String, String)} but for date headers. + * Return all values of a given header name, + * even if this header is set multiple times. + * @param headerName the header name + * @return all associated values + * @since 4.3 + */ + public List<String> getValuesAsList(String headerName) { + List<String> values = get(headerName); + if (values != null) { + List<String> result = new ArrayList<String>(); + for (String value : values) { + if (value != null) { + String[] tokens = StringUtils.tokenizeToStringArray(value, ","); + for (String token : tokens) { + result.add(token); + } + } + } + return result; + } + return Collections.emptyList(); + } + + /** + * Retrieve a combined result from the field values of the ETag header. + * @param headerName the header name + * @return the combined result + * @since 4.3 + */ + protected List<String> getETagValuesAsList(String headerName) { + List<String> values = get(headerName); + if (values != null) { + List<String> result = new ArrayList<String>(); + for (String value : values) { + if (value != null) { + Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); + while (matcher.find()) { + if ("*".equals(matcher.group())) { + result.add(matcher.group()); + } + else { + result.add(matcher.group(1)); + } + } + if (result.isEmpty()) { + throw new IllegalArgumentException( + "Could not parse header '" + headerName + "' with value '" + value + "'"); + } + } + } + return result; + } + return Collections.emptyList(); + } + + /** + * Retrieve a combined result from the field values of multi-valued headers. + * @param headerName the header name + * @return the combined result + * @since 4.3 */ - public void setDate(String headerName, long date) { - SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); - dateFormat.setTimeZone(GMT); - set(headerName, dateFormat.format(new Date(date))); + protected String getFieldValues(String headerName) { + List<String> headerValues = get(headerName); + return (headerValues != null ? toCommaDelimitedString(headerValues) : null); + } + + /** + * Turn the given list of header values into a comma-delimited result. + * @param headerValues the list of header values + * @return a combined result with comma delimitation + */ + protected String toCommaDelimitedString(List<String> headerValues) { + StringBuilder builder = new StringBuilder(); + for (Iterator<String> it = headerValues.iterator(); it.hasNext(); ) { + String val = it.next(); + builder.append(val); + if (it.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); } + + // MultiValueMap implementation + /** * Return the first header value for the given header name, if any. * @param headerName the header name diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index 87173fd3..c38c46fc 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -61,7 +61,7 @@ public enum HttpMethod { * @since 4.2.4 */ public boolean matches(String method) { - return name().equals(method); + return (this == resolve(method)); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpRange.java b/spring-web/src/main/java/org/springframework/http/HttpRange.java index 29f2e675..63d40e69 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpRange.java +++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,16 @@ package org.springframework.http; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourceRegion; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -42,6 +46,30 @@ public abstract class HttpRange { /** + * Turn a {@code Resource} into a {@link ResourceRegion} using the range + * information contained in the current {@code HttpRange}. + * @param resource the {@code Resource} to select the region from + * @return the selected region of the given {@code Resource} + * @since 4.3 + */ + public ResourceRegion toResourceRegion(Resource resource) { + // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... + // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! + Assert.isTrue(InputStreamResource.class != resource.getClass(), + "Can't convert an InputStreamResource to a ResourceRegion"); + try { + long contentLength = resource.contentLength(); + Assert.isTrue(contentLength > 0, "Resource content length should be > 0"); + long start = getRangeStart(contentLength); + long end = getRangeEnd(contentLength); + return new ResourceRegion(resource, start, end - start + 1); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to convert Resource to ResourceRegion", ex); + } + } + + /** * Return the start of the range given the total length of a representation. * @param length the length of the representation * @return the start of this range for the representation @@ -134,6 +162,25 @@ public abstract class HttpRange { } /** + * Convert each {@code HttpRange} into a {@code ResourceRegion}, + * selecting the appropriate segment of the given {@code Resource} + * using the HTTP Range information. + * @param ranges the list of ranges + * @param resource the resource to select the regions from + * @return the list of regions for the given resource + */ + public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) { + if(ranges == null || ranges.size() == 0) { + return Collections.emptyList(); + } + List<ResourceRegion> regions = new ArrayList<ResourceRegion>(ranges.size()); + for(HttpRange range : ranges) { + regions.add(range.toResourceRegion(resource)); + } + return regions; + } + + /** * Return a string representation of the given list of {@code HttpRange} objects. * <p>This method can be used to for an {@code Range} header. * @param ranges the ranges to create a string of diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index d0f9b7c4..2c864624 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,14 @@ package org.springframework.http; /** - * Java 5 enumeration of HTTP status codes. + * Enumeration of HTTP status codes. * * <p>The HTTP status code series can be retrieved via {@link #series()}. * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel + * @since 3.0 * @see HttpStatus.Series * @see <a href="http://www.iana.org/assignments/http-status-codes">HTTP Status Code Registry</a> * @see <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes">List of HTTP status codes - Wikipedia</a> @@ -321,6 +323,13 @@ public enum HttpStatus { * @see <a href="http://tools.ietf.org/html/rfc6585#section-5">Additional HTTP Status Codes</a> */ REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), + /** + * {@code 451 Unavailable For Legal Reasons}. + * @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-legally-restricted-status-04"> + * An HTTP Status Code to Report Legal Obstacles</a> + * @since 4.3 + */ + UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), // --- 5xx Server Error --- @@ -385,17 +394,17 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"); - private final int value; private final String reasonPhrase; - private HttpStatus(int value, String reasonPhrase) { + HttpStatus(int value, String reasonPhrase) { this.value = value; this.reasonPhrase = reasonPhrase; } + /** * Return the integer value of this status code. */ @@ -407,7 +416,7 @@ public enum HttpStatus { * Return the reason phrase of this status code. */ public String getReasonPhrase() { - return reasonPhrase; + return this.reasonPhrase; } /** @@ -416,7 +425,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is1xxInformational() { - return (Series.INFORMATIONAL.equals(series())); + return Series.INFORMATIONAL.equals(series()); } /** @@ -425,7 +434,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is2xxSuccessful() { - return (Series.SUCCESSFUL.equals(series())); + return Series.SUCCESSFUL.equals(series()); } /** @@ -434,7 +443,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is3xxRedirection() { - return (Series.REDIRECTION.equals(series())); + return Series.REDIRECTION.equals(series()); } @@ -444,7 +453,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is4xxClientError() { - return (Series.CLIENT_ERROR.equals(series())); + return Series.CLIENT_ERROR.equals(series()); } /** @@ -453,7 +462,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is5xxServerError() { - return (Series.SERVER_ERROR.equals(series())); + return Series.SERVER_ERROR.equals(series()); } /** @@ -469,7 +478,7 @@ public enum HttpStatus { */ @Override public String toString() { - return Integer.toString(value); + return Integer.toString(this.value); } @@ -490,10 +499,10 @@ public enum HttpStatus { /** - * Java 5 enumeration of HTTP status series. + * Enumeration of HTTP status series. * <p>Retrievable via {@link HttpStatus#series()}. */ - public static enum Series { + public enum Series { INFORMATIONAL(1), SUCCESSFUL(2), @@ -503,7 +512,7 @@ public enum HttpStatus { private final int value; - private Series(int value) { + Series(int value) { this.value = value; } @@ -527,7 +536,6 @@ public enum HttpStatus { public static Series valueOf(HttpStatus status) { return valueOf(status.value); } - } } diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 8b8e0485..924c1e4c 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -42,8 +43,7 @@ import org.springframework.util.comparator.CompoundComparator; * @author Rossen Stoyanchev * @author Sebastien Deleuze * @since 3.0 - * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics - * and Content, section 3.1.1.1</a> + * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics and Content, section 3.1.1.1</a> */ public class MediaType extends MimeType implements Serializable { @@ -71,7 +71,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code application/x-www-form-urlencoded}. - * */ + */ public final static MediaType APPLICATION_FORM_URLENCODED; /** @@ -103,7 +103,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code application/octet-stream}. - * */ + */ public final static MediaType APPLICATION_OCTET_STREAM; /** @@ -112,8 +112,18 @@ public class MediaType extends MimeType implements Serializable { public final static String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; /** + * Public constant media type for {@code application/pdf}. + */ + public final static MediaType APPLICATION_PDF; + + /** + * A String equivalent of {@link MediaType#APPLICATION_PDF}. + */ + public final static String APPLICATION_PDF_VALUE = "application/pdf"; + + /** * Public constant media type for {@code application/xhtml+xml}. - * */ + */ public final static MediaType APPLICATION_XHTML_XML; /** @@ -163,7 +173,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code multipart/form-data}. - * */ + */ public final static MediaType MULTIPART_FORM_DATA; /** @@ -173,7 +183,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code text/html}. - * */ + */ public final static MediaType TEXT_HTML; /** @@ -182,8 +192,18 @@ public class MediaType extends MimeType implements Serializable { public final static String TEXT_HTML_VALUE = "text/html"; /** + * Public constant media type for {@code text/markdown}. + */ + public final static MediaType TEXT_MARKDOWN; + + /** + * A String equivalent of {@link MediaType#TEXT_MARKDOWN}. + */ + public final static String TEXT_MARKDOWN_VALUE = "text/markdown"; + + /** * Public constant media type for {@code text/plain}. - * */ + */ public final static MediaType TEXT_PLAIN; /** @@ -193,7 +213,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code text/xml}. - * */ + */ public final static MediaType TEXT_XML; /** @@ -212,6 +232,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE); APPLICATION_JSON_UTF8 = valueOf(APPLICATION_JSON_UTF8_VALUE); APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE); + APPLICATION_PDF = valueOf(APPLICATION_PDF_VALUE); APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE); APPLICATION_XML = valueOf(APPLICATION_XML_VALUE); IMAGE_GIF = valueOf(IMAGE_GIF_VALUE); @@ -219,6 +240,7 @@ public class MediaType extends MimeType implements Serializable { IMAGE_PNG = valueOf(IMAGE_PNG_VALUE); MULTIPART_FORM_DATA = valueOf(MULTIPART_FORM_DATA_VALUE); TEXT_HTML = valueOf(TEXT_HTML_VALUE); + TEXT_MARKDOWN = valueOf(TEXT_MARKDOWN_VALUE); TEXT_PLAIN = valueOf(TEXT_PLAIN_VALUE); TEXT_XML = valueOf(TEXT_XML_VALUE); } @@ -268,6 +290,17 @@ public class MediaType extends MimeType implements Serializable { } /** + * Copy-constructor that copies the type, subtype and parameters of the given + * {@code MediaType}, and allows to set the specified character set. + * @param other the other media type + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(MediaType other, Charset charset) { + super(other, charset); + } + + /** * Copy-constructor that copies the type and subtype of the given {@code MediaType}, * and allows for different parameter. * @param other the other media type @@ -364,6 +397,8 @@ public class MediaType extends MimeType implements Serializable { * Parse the given String value into a {@code MediaType} object, * with this method name following the 'valueOf' naming convention * (as supported by {@link org.springframework.core.convert.ConversionService}. + * @param value the string to parse + * @throws InvalidMediaTypeException if the media type value cannot be parsed * @see #parseMediaType(String) */ public static MediaType valueOf(String value) { @@ -374,7 +409,7 @@ public class MediaType extends MimeType implements Serializable { * Parse the given String into a single {@code MediaType}. * @param mediaType the string to parse * @return the media type - * @throws InvalidMediaTypeException if the string cannot be parsed + * @throws InvalidMediaTypeException if the media type value cannot be parsed */ public static MediaType parseMediaType(String mediaType) { MimeType type; @@ -392,13 +427,12 @@ public class MediaType extends MimeType implements Serializable { } } - /** - * Parse the given, comma-separated string into a list of {@code MediaType} objects. + * Parse the given comma-separated string into a list of {@code MediaType} objects. * <p>This method can be used to parse an Accept or Content-Type header. * @param mediaTypes the string to parse * @return the list of media types - * @throws IllegalArgumentException if the string cannot be parsed + * @throws InvalidMediaTypeException if the media type value cannot be parsed */ public static List<MediaType> parseMediaTypes(String mediaTypes) { if (!StringUtils.hasLength(mediaTypes)) { @@ -413,6 +447,31 @@ public class MediaType extends MimeType implements Serializable { } /** + * Parse the given list of (potentially) comma-separated strings into a + * list of {@code MediaType} objects. + * <p>This method can be used to parse an Accept or Content-Type header. + * @param mediaTypes the string to parse + * @return the list of media types + * @throws InvalidMediaTypeException if the media type value cannot be parsed + * @since 4.3.2 + */ + public static List<MediaType> parseMediaTypes(List<String> mediaTypes) { + if (CollectionUtils.isEmpty(mediaTypes)) { + return Collections.<MediaType>emptyList(); + } + else if (mediaTypes.size() == 1) { + return parseMediaTypes(mediaTypes.get(0)); + } + else { + List<MediaType> result = new ArrayList<MediaType>(8); + for (String mediaType : mediaTypes) { + result.addAll(parseMediaTypes(mediaType)); + } + return result; + } + } + + /** * Return a string representation of the given list of {@code MediaType} objects. * <p>This method can be used to for an {@code Accept} or {@code Content-Type} header. * @param mediaTypes the media types to create a string representation for diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index 7e9ab88b..46dbba93 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.http; +import java.lang.reflect.Type; import java.net.URI; import java.nio.charset.Charset; import java.util.Arrays; @@ -54,6 +55,7 @@ import org.springframework.util.ObjectUtils; * </pre> * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 4.1 * @see #getMethod() * @see #getUrl() @@ -64,6 +66,8 @@ public class RequestEntity<T> extends HttpEntity<T> { private final URI url; + private final Type type; + /** * Constructor with method and URL but without body nor headers. @@ -81,7 +85,19 @@ public class RequestEntity<T> extends HttpEntity<T> { * @param url the URL */ public RequestEntity(T body, HttpMethod method, URI url) { - this(body, null, method, url); + this(body, null, method, url, null); + } + + /** + * Constructor with method, URL, body and type but without headers. + * @param body the body + * @param method the method + * @param url the URL + * @param type the type used for generic type resolution + * @since 4.3 + */ + public RequestEntity(T body, HttpMethod method, URI url, Type type) { + this(body, null, method, url, type); } /** @@ -91,7 +107,7 @@ public class RequestEntity<T> extends HttpEntity<T> { * @param url the URL */ public RequestEntity(MultiValueMap<String, String> headers, HttpMethod method, URI url) { - this(null, headers, method, url); + this(null, headers, method, url, null); } /** @@ -102,9 +118,23 @@ public class RequestEntity<T> extends HttpEntity<T> { * @param url the URL */ public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url) { + this(body, headers, method, url, null); + } + + /** + * Constructor with method, URL, headers, body and type. + * @param body the body + * @param headers the headers + * @param method the method + * @param url the URL + * @param type the type used for generic type resolution + * @since 4.3 + */ + public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url, Type type) { super(body, headers); this.method = method; this.url = url; + this.type = type; } @@ -124,6 +154,21 @@ public class RequestEntity<T> extends HttpEntity<T> { return this.url; } + /** + * Return the type of the request's body. + * @return the request's body type, or {@code null} if not known + * @since 4.3 + */ + public Type getType() { + if (this.type == null) { + T body = getBody(); + if (body != null) { + return body.getClass(); + } + } + return this.type; + } + @Override public boolean equals(Object other) { @@ -134,8 +179,8 @@ public class RequestEntity<T> extends HttpEntity<T> { return false; } RequestEntity<?> otherEntity = (RequestEntity<?>) other; - return (ObjectUtils.nullSafeEquals(this.method, otherEntity.method) && - ObjectUtils.nullSafeEquals(this.url, otherEntity.url)); + return (ObjectUtils.nullSafeEquals(getMethod(), otherEntity.getMethod()) && + ObjectUtils.nullSafeEquals(getUrl(), otherEntity.getUrl())); } @Override @@ -149,9 +194,9 @@ public class RequestEntity<T> extends HttpEntity<T> { @Override public String toString() { StringBuilder builder = new StringBuilder("<"); - builder.append(this.method); + builder.append(getMethod()); builder.append(' '); - builder.append(this.url); + builder.append(getUrl()); builder.append(','); T body = getBody(); HttpHeaders headers = getHeaders(); @@ -327,6 +372,16 @@ public class RequestEntity<T> extends HttpEntity<T> { * @return the built request entity */ <T> RequestEntity<T> body(T body); + + /** + * Set the body and type of the request entity and build the RequestEntity. + * @param <T> the type of the body + * @param body the body of the request entity + * @param type the type of the body, useful for generic type resolution + * @return the built request entity + * @since 4.3 + */ + <T> RequestEntity<T> body(T body, Type type); } @@ -396,6 +451,11 @@ public class RequestEntity<T> extends HttpEntity<T> { public <T> RequestEntity<T> body(T body) { return new RequestEntity<T>(body, this.headers, this.method, this.url); } + + @Override + public <T> RequestEntity<T> body(T body, Type type) { + return new RequestEntity<T>(body, this.headers, this.method, this.url, type); + } } } diff --git a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java index 9eba72c0..7edb9ea2 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; @@ -65,45 +66,55 @@ import org.springframework.util.ObjectUtils; */ public class ResponseEntity<T> extends HttpEntity<T> { - private final HttpStatus statusCode; + private final Object statusCode; /** * Create a new {@code ResponseEntity} with the given status code, and no body nor headers. - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(HttpStatus statusCode) { - super(); - this.statusCode = statusCode; + public ResponseEntity(HttpStatus status) { + this(null, null, status); } /** * Create a new {@code ResponseEntity} with the given body and status code, and no headers. * @param body the entity body - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(T body, HttpStatus statusCode) { - super(body); - this.statusCode = statusCode; + public ResponseEntity(T body, HttpStatus status) { + this(body, null, status); } /** * Create a new {@code HttpEntity} with the given headers and status code, and no body. * @param headers the entity headers - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus statusCode) { - super(headers); - this.statusCode = statusCode; + public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status) { + this(null, headers, status); } /** * Create a new {@code HttpEntity} with the given body, headers, and status code. * @param body the entity body * @param headers the entity headers - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus statusCode) { + public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status) { + super(body, headers); + Assert.notNull(status, "HttpStatus must not be null"); + this.statusCode = status; + } + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * Just used behind the nested builder API. + * @param body the entity body + * @param headers the entity headers + * @param statusCode the status code (as {@code HttpStatus} or as {@code Integer} value) + */ + private ResponseEntity(T body, MultiValueMap<String, String> headers, Object statusCode) { super(body, headers); this.statusCode = statusCode; } @@ -111,10 +122,29 @@ public class ResponseEntity<T> extends HttpEntity<T> { /** * Return the HTTP status code of the response. - * @return the HTTP status as an HttpStatus enum value + * @return the HTTP status as an HttpStatus enum entry */ public HttpStatus getStatusCode() { - return this.statusCode; + if (this.statusCode instanceof HttpStatus) { + return (HttpStatus) this.statusCode; + } + else { + return HttpStatus.valueOf((Integer) this.statusCode); + } + } + + /** + * Return the HTTP status code of the response. + * @return the HTTP status as an int value + * @since 4.3 + */ + public int getStatusCodeValue() { + if (this.statusCode instanceof HttpStatus) { + return ((HttpStatus) this.statusCode).value(); + } + else { + return (Integer) this.statusCode; + } } @@ -139,8 +169,10 @@ public class ResponseEntity<T> extends HttpEntity<T> { public String toString() { StringBuilder builder = new StringBuilder("<"); builder.append(this.statusCode.toString()); - builder.append(' '); - builder.append(this.statusCode.getReasonPhrase()); + if (this.statusCode instanceof HttpStatus) { + builder.append(' '); + builder.append(((HttpStatus) this.statusCode).getReasonPhrase()); + } builder.append(','); T body = getBody(); HttpHeaders headers = getHeaders(); @@ -167,6 +199,7 @@ public class ResponseEntity<T> extends HttpEntity<T> { * @since 4.1 */ public static BodyBuilder status(HttpStatus status) { + Assert.notNull(status, "HttpStatus must not be null"); return new DefaultBuilder(status); } @@ -177,7 +210,7 @@ public class ResponseEntity<T> extends HttpEntity<T> { * @since 4.1 */ public static BodyBuilder status(int status) { - return status(HttpStatus.valueOf(status)); + return new DefaultBuilder(status); } /** @@ -333,6 +366,17 @@ public class ResponseEntity<T> extends HttpEntity<T> { B cacheControl(CacheControl cacheControl); /** + * Configure one or more request header names (e.g. "Accept-Language") to + * add to the "Vary" response header to inform clients that the response is + * subject to content negotiation and variances based on the value of the + * given request headers. The configured request header names are added only + * if not already present in the response "Vary" header. + * @param requestHeaders request header names + * @since 4.3 + */ + B varyBy(String... requestHeaders); + + /** * Build the response entity with no body. * @return the response entity * @see BodyBuilder#body(Object) @@ -377,12 +421,12 @@ public class ResponseEntity<T> extends HttpEntity<T> { private static class DefaultBuilder implements BodyBuilder { - private final HttpStatus status; + private final Object statusCode; private final HttpHeaders headers = new HttpHeaders(); - public DefaultBuilder(HttpStatus status) { - this.status = status; + public DefaultBuilder(Object statusCode) { + this.statusCode = statusCode; } @Override @@ -455,13 +499,19 @@ public class ResponseEntity<T> extends HttpEntity<T> { } @Override + public BodyBuilder varyBy(String... requestHeaders) { + this.headers.setVary(Arrays.asList(requestHeaders)); + return this; + } + + @Override public ResponseEntity<Void> build() { - return new ResponseEntity<Void>(null, this.headers, this.status); + return body(null); } @Override public <T> ResponseEntity<T> body(T body) { - return new ResponseEntity<T>(body, this.headers, this.status); + return new ResponseEntity<T>(body, this.headers, this.statusCode); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java new file mode 100644 index 00000000..2f09014a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * Represents the context of a client-side HTTP request execution. + * + * <p>Used to invoke the next interceptor in the interceptor chain, or - + * if the calling interceptor is last - execute the request itself. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 + * @see AsyncClientHttpRequestInterceptor + */ +public interface AsyncClientHttpRequestExecution { + + /** + * Resume the request execution by invoking the next interceptor in the chain + * or executing the request to the remote service. + * @param request the HTTP request, containing the HTTP method and headers + * @param body the body of the request + * @return a corresponding future handle + * @throws IOException in case of I/O errors + */ + ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body) throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java new file mode 100644 index 00000000..3fabe120 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.support.InterceptingAsyncHttpAccessor; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * Intercepts client-side HTTP requests. Implementations of this interface can be + * {@linkplain org.springframework.web.client.AsyncRestTemplate#setInterceptors registered} + * with the {@link org.springframework.web.client.AsyncRestTemplate} as to modify + * the outgoing {@link HttpRequest} and/or register to modify the incoming + * {@link ClientHttpResponse} with help of a + * {@link org.springframework.util.concurrent.ListenableFutureAdapter}. + * + * <p>The main entry point for interceptors is {@link #intercept}. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 + * @see org.springframework.web.client.AsyncRestTemplate + * @see InterceptingAsyncHttpAccessor + */ +public interface AsyncClientHttpRequestInterceptor { + + /** + * Intercept the given request, and return a response future. The given + * {@link AsyncClientHttpRequestExecution} allows the interceptor to pass on + * the request to the next entity in the chain. + * <p>An implementation might follow this pattern: + * <ol> + * <li>Examine the {@linkplain HttpRequest request} and body</li> + * <li>Optionally {@linkplain org.springframework.http.client.support.HttpRequestWrapper + * wrap} the request to filter HTTP attributes.</li> + * <li>Optionally modify the body of the request.</li> + * <li>One of the following: + * <ul> + * <li>execute the request through {@link ClientHttpRequestExecution}</li> + * <li>don't execute the request to block the execution altogether</li> + * </ul> + * <li>Optionally adapt the response to filter HTTP attributes with the help of + * {@link org.springframework.util.concurrent.ListenableFutureAdapter + * ListenableFutureAdapter}.</li> + * </ol> + * @param request the request, containing method, URI, and headers + * @param body the body of the request + * @param execution the request execution + * @return the response future + * @throws IOException in case of I/O errors + */ + ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body, + AsyncClientHttpRequestExecution execution) throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java index df6e10c6..2fe64f52 100644 --- a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java +++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,22 +23,23 @@ import org.springframework.http.HttpRequest; /** * Represents the context of a client-side HTTP request execution. * - * <p>Used to invoke the next interceptor in the interceptor chain, or - if the calling interceptor is last - execute - * the request itself. + * <p>Used to invoke the next interceptor in the interceptor chain, + * or - if the calling interceptor is last - execute the request itself. * * @author Arjen Poutsma - * @see ClientHttpRequestInterceptor * @since 3.1 + * @see ClientHttpRequestInterceptor */ public interface ClientHttpRequestExecution { /** - * Execute the request with the given request attributes and body, and return the response. - * + * Execute the request with the given request attributes and body, + * and return the response. * @param request the request, containing method, URI, and headers * @param body the body of the request to execute * @return the response * @throws IOException in case of I/O errors */ ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException; + } diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index 53f6faf0..d61231d9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.apache.http.protocol.HttpContext; import org.springframework.beans.factory.DisposableBean; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that @@ -58,6 +59,20 @@ import org.springframework.util.Assert; */ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean { + private static Class<?> abstractHttpClientClass; + + static { + try { + // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3) + abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient", + HttpComponentsClientHttpRequestFactory.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Probably removed from HttpComponents in the meantime... + } + } + + private HttpClient httpClient; private RequestConfig requestConfig; @@ -130,7 +145,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest */ @SuppressWarnings("deprecation") private void setLegacyConnectionTimeout(HttpClient client, int timeout) { - if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { + if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) { client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); } } @@ -171,7 +186,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest */ @SuppressWarnings("deprecation") private void setLegacySocketTimeout(HttpClient client, int timeout) { - if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { + if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) { client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java new file mode 100644 index 00000000..0b13181a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; +import java.util.List; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.util.StreamUtils; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * An {@link AsyncClientHttpRequest} wrapper that enriches it proceeds the actual + * request execution with calling the registered interceptors. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @see InterceptingAsyncClientHttpRequestFactory + */ +class InterceptingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private AsyncClientHttpRequestFactory requestFactory; + + private List<AsyncClientHttpRequestInterceptor> interceptors; + + private URI uri; + + private HttpMethod httpMethod; + + + /** + * Creates new instance of {@link InterceptingAsyncClientHttpRequest}. + * + * @param requestFactory the async request factory + * @param interceptors the list of interceptors + * @param uri the request URI + * @param httpMethod the HTTP method + */ + public InterceptingAsyncClientHttpRequest(AsyncClientHttpRequestFactory requestFactory, + List<AsyncClientHttpRequestInterceptor> interceptors, URI uri, HttpMethod httpMethod) { + + this.requestFactory = requestFactory; + this.interceptors = interceptors; + this.uri = uri; + this.httpMethod = httpMethod; + } + + + @Override + protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] body) + throws IOException { + + return new AsyncRequestExecution().executeAsync(this, body); + } + + @Override + public HttpMethod getMethod() { + return httpMethod; + } + + @Override + public URI getURI() { + return uri; + } + + + private class AsyncRequestExecution implements AsyncClientHttpRequestExecution { + + private Iterator<AsyncClientHttpRequestInterceptor> iterator; + + public AsyncRequestExecution() { + this.iterator = interceptors.iterator(); + } + + @Override + public ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body) + throws IOException { + + if (this.iterator.hasNext()) { + AsyncClientHttpRequestInterceptor interceptor = this.iterator.next(); + return interceptor.intercept(request, body, this); + } + else { + URI theUri = request.getURI(); + HttpMethod theMethod = request.getMethod(); + HttpHeaders theHeaders = request.getHeaders(); + + AsyncClientHttpRequest delegate = requestFactory.createAsyncRequest(theUri, theMethod); + delegate.getHeaders().putAll(theHeaders); + if (body.length > 0) { + StreamUtils.copy(body, delegate.getBody()); + } + + return delegate.executeAsync(); + } + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java new file mode 100644 index 00000000..1ca68c52 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.net.URI; +import java.util.Collections; +import java.util.List; + +import org.springframework.http.HttpMethod; + +/** + * Wrapper for a {@link AsyncClientHttpRequestFactory} that has support for + * {@link AsyncClientHttpRequestInterceptor}s. + * + * @author Jakub Narloch + * @since 4.3 + * @see InterceptingAsyncClientHttpRequest + */ +public class InterceptingAsyncClientHttpRequestFactory implements AsyncClientHttpRequestFactory { + + private AsyncClientHttpRequestFactory delegate; + + private List<AsyncClientHttpRequestInterceptor> interceptors; + + + /** + * Create new instance of {@link InterceptingAsyncClientHttpRequestFactory} + * with delegated request factory and list of interceptors. + * @param delegate the request factory to delegate to + * @param interceptors the list of interceptors to use + */ + public InterceptingAsyncClientHttpRequestFactory(AsyncClientHttpRequestFactory delegate, + List<AsyncClientHttpRequestInterceptor> interceptors) { + + this.delegate = delegate; + this.interceptors = (interceptors != null ? interceptors : Collections.<AsyncClientHttpRequestInterceptor>emptyList()); + } + + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod method) { + return new InterceptingAsyncClientHttpRequest(this.delegate, this.interceptors, uri, method); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java index be619f1f..2baa53a4 100644 --- a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java @@ -56,11 +56,13 @@ class Netty4ClientHttpResponse extends AbstractClientHttpResponse { @Override + @SuppressWarnings("deprecation") public int getRawStatusCode() throws IOException { return this.nettyResponse.getStatus().code(); } @Override + @SuppressWarnings("deprecation") public String getStatusText() throws IOException { return this.nettyResponse.getStatus().reasonPhrase(); } diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java new file mode 100644 index 00000000..057a1652 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +/** + * {@link AsyncClientHttpRequest} implementation that uses OkHttp 3.x to execute requests. + * + * <p>Created via the {@link OkHttp3ClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +class OkHttp3AsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttp3AsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content) + throws IOException { + + Request request = OkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttpListenableFuture(this.client.newCall(request)); + } + + + private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> { + + private final Call call; + + public OkHttpListenableFuture(Call call) { + this.call = call; + this.call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + set(new OkHttp3ClientHttpResponse(response)); + } + @Override + public void onFailure(Call call, IOException ex) { + setException(ex); + } + }); + } + + @Override + protected void interruptTask() { + this.call.cancel(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java new file mode 100644 index 00000000..e0ebaac3 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * {@link ClientHttpRequest} implementation that uses OkHttp 3.x to execute requests. + * + * <p>Created via the {@link OkHttp3ClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +class OkHttp3ClientHttpRequest extends AbstractBufferingClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttp3ClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + + @Override + protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException { + Request request = OkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttp3ClientHttpResponse(this.client.newCall(request).execute()); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java new file mode 100644 index 00000000..546fde34 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link ClientHttpRequestFactory} implementation that uses + * <a href="http://square.github.io/okhttp/">OkHttp</a> 3.x to create requests. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +public class OkHttp3ClientHttpRequestFactory + implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean { + + private OkHttpClient client; + + private final boolean defaultClient; + + + /** + * Create a factory with a default {@link OkHttpClient} instance. + */ + public OkHttp3ClientHttpRequestFactory() { + this.client = new OkHttpClient(); + this.defaultClient = true; + } + + /** + * Create a factory with the given {@link OkHttpClient} instance. + * @param client the client to use + */ + public OkHttp3ClientHttpRequestFactory(OkHttpClient client) { + Assert.notNull(client, "OkHttpClient must not be null"); + this.client = client; + this.defaultClient = false; + } + + + /** + * Sets the underlying read timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * @see okhttp3.OkHttpClient.Builder#readTimeout(long, TimeUnit) + */ + public void setReadTimeout(int readTimeout) { + this.client = this.client.newBuilder() + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Sets the underlying write timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * @see okhttp3.OkHttpClient.Builder#writeTimeout(long, TimeUnit) + */ + public void setWriteTimeout(int writeTimeout) { + this.client = this.client.newBuilder() + .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Sets the underlying connect timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * @see okhttp3.OkHttpClient.Builder#connectTimeout(long, TimeUnit) + */ + public void setConnectTimeout(int connectTimeout) { + this.client = this.client.newBuilder() + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { + return new OkHttp3ClientHttpRequest(this.client, uri, httpMethod); + } + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) { + return new OkHttp3AsyncClientHttpRequest(this.client, uri, httpMethod); + } + + + @Override + public void destroy() throws IOException { + if (this.defaultClient) { + // Clean up the client if we created it in the constructor + if (this.client.cache() != null) { + this.client.cache().close(); + } + this.client.dispatcher().executorService().shutdown(); + } + } + + + static Request buildRequest(HttpHeaders headers, byte[] content, URI uri, + HttpMethod method) throws MalformedURLException { + + okhttp3.MediaType contentType = getContentType(headers); + RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null); + + URL url = uri.toURL(); + String methodName = method.name(); + Request.Builder builder = new Request.Builder().url(url).method(methodName, body); + + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + builder.addHeader(headerName, headerValue); + } + } + + return builder.build(); + } + + private static okhttp3.MediaType getContentType(HttpHeaders headers) { + String rawContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + return (StringUtils.hasText(rawContentType) ? okhttp3.MediaType.parse(rawContentType) : null); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java new file mode 100644 index 00000000..b6a7ed19 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.io.InputStream; + +import okhttp3.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +/** + * {@link ClientHttpResponse} implementation based on OkHttp 3.x. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +class OkHttp3ClientHttpResponse extends AbstractClientHttpResponse { + + private final Response response; + + private HttpHeaders headers; + + + public OkHttp3ClientHttpResponse(Response response) { + Assert.notNull(response, "Response must not be null"); + this.response = response; + } + + + @Override + public int getRawStatusCode() { + return this.response.code(); + } + + @Override + public String getStatusText() { + return this.response.message(); + } + + @Override + public InputStream getBody() throws IOException { + return this.response.body().byteStream(); + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + HttpHeaders headers = new HttpHeaders(); + for (String headerName : this.response.headers().names()) { + for (String headerValue : this.response.headers(headerName)) { + headers.add(headerName, headerValue); + } + } + this.headers = headers; + } + return this.headers; + } + + @Override + public void close() { + this.response.body().close(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java new file mode 100644 index 00000000..6b042422 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; + +import com.squareup.okhttp.Call; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +/** + * {@link AsyncClientHttpRequest} implementation that uses OkHttp 2.x to execute requests. + * + * <p>Created via the {@link OkHttpClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @since 4.3 + * @see org.springframework.http.client.OkHttp3AsyncClientHttpRequest + */ +class OkHttpAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttpAsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content) + throws IOException { + + Request request = OkHttpClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttpListenableFuture(this.client.newCall(request)); + } + + + private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> { + + private final Call call; + + public OkHttpListenableFuture(Call call) { + this.call = call; + this.call.enqueue(new Callback() { + @Override + public void onResponse(Response response) { + set(new OkHttpClientHttpResponse(response)); + } + @Override + public void onFailure(Request request, IOException ex) { + setException(ex); + } + }); + } + + @Override + protected void interruptTask() { + this.call.cancel(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java index fe519f06..a2f75be4 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,35 +18,24 @@ package org.springframework.http.client; import java.io.IOException; import java.net.URI; -import java.net.URL; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -import com.squareup.okhttp.Call; -import com.squareup.okhttp.Callback; -import com.squareup.okhttp.MediaType; + import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; -import com.squareup.okhttp.Response; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; /** - * {@link ClientHttpRequest} implementation that uses OkHttp to execute requests. + * {@link ClientHttpRequest} implementation that uses OkHttp 2.x to execute requests. * * <p>Created via the {@link OkHttpClientHttpRequestFactory}. * * @author Luciano Leggieri * @author Arjen Poutsma * @since 4.2 + * @see org.springframework.http.client.OkHttp3ClientHttpRequest */ -class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest implements ClientHttpRequest { +class OkHttpClientHttpRequest extends AbstractBufferingClientHttpRequest { private final OkHttpClient client; @@ -72,73 +61,11 @@ class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest im return this.uri; } - @Override - protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content) - throws IOException { - - MediaType contentType = getContentType(headers); - RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null); - - URL url = this.uri.toURL(); - String methodName = this.method.name(); - Request.Builder builder = new Request.Builder().url(url).method(methodName, body); - - for (Map.Entry<String, List<String>> entry : headers.entrySet()) { - String headerName = entry.getKey(); - for (String headerValue : entry.getValue()) { - builder.addHeader(headerName, headerValue); - } - } - Request request = builder.build(); - - return new OkHttpListenableFuture(this.client.newCall(request)); - } - - private MediaType getContentType(HttpHeaders headers) { - String rawContentType = headers.getFirst("Content-Type"); - return (StringUtils.hasText(rawContentType) ? MediaType.parse(rawContentType) : null); - } @Override - public ClientHttpResponse execute() throws IOException { - try { - return executeAsync().get(); - } - catch (InterruptedException ex) { - throw new IOException(ex.getMessage(), ex); - } - catch (ExecutionException ex) { - Throwable cause = ex.getCause(); - if (cause instanceof IOException) { - throw (IOException) cause; - } - throw new IOException(cause.getMessage(), cause); - } - } - - - private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> { - - private final Call call; - - public OkHttpListenableFuture(Call call) { - this.call = call; - this.call.enqueue(new Callback() { - @Override - public void onResponse(Response response) { - set(new OkHttpClientHttpResponse(response)); - } - @Override - public void onFailure(Request request, IOException ex) { - setException(ex); - } - }); - } - - @Override - protected void interruptTask() { - this.call.cancel(); - } + protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException { + Request request = OkHttpClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttpClientHttpResponse(this.client.newCall(request).execute()); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java index 9b2674dd..d5ae9e97 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java @@ -16,22 +16,32 @@ package org.springframework.http.client; +import java.io.IOException; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link ClientHttpRequestFactory} implementation that uses - * <a href="http://square.github.io/okhttp/">OkHttp</a> to create requests. + * <a href="http://square.github.io/okhttp/">OkHttp</a> 2.x to create requests. * * @author Luciano Leggieri * @author Arjen Poutsma * @since 4.2 + * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory */ public class OkHttpClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean { @@ -90,20 +100,17 @@ public class OkHttpClientHttpRequestFactory @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { - return createRequestInternal(uri, httpMethod); + return new OkHttpClientHttpRequest(this.client, uri, httpMethod); } @Override public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) { - return createRequestInternal(uri, httpMethod); + return new OkHttpAsyncClientHttpRequest(this.client, uri, httpMethod); } - private OkHttpClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { - return new OkHttpClientHttpRequest(this.client, uri, httpMethod); - } @Override - public void destroy() throws Exception { + public void destroy() throws IOException { if (this.defaultClient) { // Clean up the client if we created it in the constructor if (this.client.getCache() != null) { @@ -113,4 +120,31 @@ public class OkHttpClientHttpRequestFactory } } + + static Request buildRequest(HttpHeaders headers, byte[] content, URI uri, + HttpMethod method) throws MalformedURLException { + + com.squareup.okhttp.MediaType contentType = getContentType(headers); + RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null); + + URL url = uri.toURL(); + String methodName = method.name(); + Request.Builder builder = new Request.Builder().url(url).method(methodName, body); + + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + builder.addHeader(headerName, headerValue); + } + } + + return builder.build(); + } + + private static com.squareup.okhttp.MediaType getContentType(HttpHeaders headers) { + String rawContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + return (StringUtils.hasText(rawContentType) ? + com.squareup.okhttp.MediaType.parse(rawContentType) : null); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java index 392a2d7d..6a0639f5 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java @@ -25,11 +25,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; /** - * {@link ClientHttpResponse} implementation based on OkHttp. + * {@link ClientHttpResponse} implementation based on OkHttp 2.x. * * @author Luciano Leggieri * @author Arjen Poutsma * @since 4.2 + * @see org.springframework.http.client.OkHttp3ClientHttpResponse */ class OkHttpClientHttpResponse extends AbstractClientHttpResponse { diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java index 015a2e42..bd439963 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,6 +89,10 @@ final class SimpleBufferingAsyncClientHttpRequest extends AbstractBufferingAsync if (connection.getDoOutput()) { FileCopyUtils.copy(bufferedOutput, connection.getOutputStream()); } + else { + // Immediately trigger the request in a no-output scenario as well + connection.getResponseCode(); + } return new SimpleClientHttpResponse(connection); } }); diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java index 24195beb..9a992f33 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,12 +68,10 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp @Override protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException { addHeaders(this.connection, headers); - // JDK <1.8 doesn't support getOutputStream with HTTP DELETE if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) { this.connection.setDoOutput(false); } - if (this.connection.getDoOutput() && this.outputStreaming) { this.connection.setFixedLengthStreamingMode(bufferedOutput.length); } @@ -81,7 +79,10 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp if (this.connection.getDoOutput()) { FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream()); } - + else { + // Immediately trigger the request in a no-output scenario as well + this.connection.getResponseCode(); + } return new SimpleClientHttpResponse(this.connection); } diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java index cc7e627d..f667cb2d 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.io.InputStream; import java.net.HttpURLConnection; import org.springframework.http.HttpHeaders; +import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** @@ -29,6 +30,7 @@ import org.springframework.util.StringUtils; * {@link SimpleStreamingClientHttpRequest#execute()}. * * @author Arjen Poutsma + * @author Brian Clozel * @since 3.0 */ final class SimpleClientHttpResponse extends AbstractClientHttpResponse { @@ -37,6 +39,8 @@ final class SimpleClientHttpResponse extends AbstractClientHttpResponse { private HttpHeaders headers; + private InputStream responseStream; + SimpleClientHttpResponse(HttpURLConnection connection) { this.connection = connection; @@ -78,12 +82,19 @@ final class SimpleClientHttpResponse extends AbstractClientHttpResponse { @Override public InputStream getBody() throws IOException { InputStream errorStream = this.connection.getErrorStream(); - return (errorStream != null ? errorStream : this.connection.getInputStream()); + this.responseStream = (errorStream != null ? errorStream : this.connection.getInputStream()); + return this.responseStream; } @Override public void close() { - this.connection.disconnect(); + if (this.responseStream != null) { + try { + StreamUtils.drain(this.responseStream); + this.responseStream.close(); + } + catch (IOException e) { } + } } } diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java index 0417eef0..e3327d23 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,6 +108,8 @@ final class SimpleStreamingAsyncClientHttpRequest extends AbstractAsyncClientHtt else { SimpleBufferingClientHttpRequest.addHeaders(connection, headers); connection.connect(); + // Immediately trigger the request in a no-output scenario as well + connection.getResponseCode(); } } catch (IOException ex) { diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java index 5e871d00..98ca2902 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,6 +94,8 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest { else { SimpleBufferingClientHttpRequest.addHeaders(this.connection, headers); this.connection.connect(); + // Immediately trigger the request in a no-output scenario as well + this.connection.getResponseCode(); } } catch (IOException ex) { diff --git a/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java new file mode 100644 index 00000000..ebf4a536 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client.support; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; + +/** + * {@link ClientHttpRequestInterceptor} to apply a BASIC authorization header. + * + * @author Phillip Webb + * @since 4.3.1 + */ +public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final String username; + + private final String password; + + + /** + * Create a new interceptor which adds a BASIC authorization header + * for the given username and password. + * @param username the username to use + * @param password the password to use + */ + public BasicAuthorizationInterceptor(String username, String password) { + Assert.hasLength(username, "Username must not be empty"); + this.username = username; + this.password = (password != null ? password : ""); + } + + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + + String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(UTF_8)); + request.getHeaders().add("Authorization", "Basic " + token); + return execution.execute(request, body); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java new file mode 100644 index 00000000..fd59f604 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.http.client.AsyncClientHttpRequestFactory; +import org.springframework.http.client.AsyncClientHttpRequestInterceptor; +import org.springframework.http.client.InterceptingAsyncClientHttpRequestFactory; +import org.springframework.util.CollectionUtils; + +/** + * The HTTP accessor that extends the base {@link AsyncHttpAccessor} with + * request intercepting functionality. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class InterceptingAsyncHttpAccessor extends AsyncHttpAccessor { + + private List<AsyncClientHttpRequestInterceptor> interceptors = + new ArrayList<AsyncClientHttpRequestInterceptor>(); + + + /** + * Set the request interceptors that this accessor should use. + * @param interceptors the list of interceptors + */ + public void setInterceptors(List<AsyncClientHttpRequestInterceptor> interceptors) { + this.interceptors = interceptors; + } + + /** + * Return the request interceptor that this accessor uses. + */ + public List<AsyncClientHttpRequestInterceptor> getInterceptors() { + return this.interceptors; + } + + + @Override + public AsyncClientHttpRequestFactory getAsyncRequestFactory() { + AsyncClientHttpRequestFactory delegate = super.getAsyncRequestFactory(); + if (!CollectionUtils.isEmpty(getInterceptors())) { + return new InterceptingAsyncClientHttpRequestFactory(delegate, getInterceptors()); + } + else { + return delegate; + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index d63d2870..e990fcd7 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -18,6 +18,7 @@ package org.springframework.http.converter; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -42,6 +43,7 @@ import org.springframework.util.Assert; * * @author Arjen Poutsma * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 3.0 */ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> { @@ -51,6 +53,8 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv private List<MediaType> supportedMediaTypes = Collections.emptyList(); + private Charset defaultCharset; + /** * Construct an {@code AbstractHttpMessageConverter} with no supported media types. @@ -75,6 +79,18 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); } + /** + * Construct an {@code AbstractHttpMessageConverter} with a default charset and + * multiple supported media types. + * @param defaultCharset the default character set + * @param supportedMediaTypes the supported media types + * @since 4.3 + */ + protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) { + this.defaultCharset = defaultCharset; + setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); + } + /** * Set the list of {@link MediaType} objects supported by this converter. @@ -89,6 +105,22 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv return Collections.unmodifiableList(this.supportedMediaTypes); } + /** + * Set the default character set, if any. + * @since 4.3 + */ + public void setDefaultCharset(Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Return the default character set, if any. + * @since 4.3 + */ + public Charset getDefaultCharset() { + return this.defaultCharset; + } + /** * This implementation checks if the given class is {@linkplain #supports(Class) supported}, @@ -200,7 +232,8 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv /** * Add default headers to the output message. * <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content - * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers. + * type was not provided, set if necessary the default character set, calls + * {@link #getContentLength}, and sets the corresponding headers. * @since 4.2 */ protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{ @@ -214,6 +247,12 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); } if (contentTypeToUse != null) { + if (contentTypeToUse.getCharset() == null) { + Charset defaultCharset = getDefaultCharset(); + if (defaultCharset != null) { + contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset); + } + } headers.setContentType(contentTypeToUse); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index fa2d0e4a..643a8713 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -82,6 +82,7 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.0 * @see MultiValueMap */ @@ -90,14 +91,14 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); - private Charset charset = DEFAULT_CHARSET; - - private Charset multipartCharset; - private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>(); private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>(); + private Charset charset = DEFAULT_CHARSET; + + private Charset multipartCharset; + public FormHttpMessageConverter() { this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); @@ -108,30 +109,10 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue stringHttpMessageConverter.setWriteAcceptCharset(false); this.partConverters.add(stringHttpMessageConverter); this.partConverters.add(new ResourceHttpMessageConverter()); - } - - /** - * Set the default character set to use for reading and writing form data when - * the request or response Content-Type header does not explicitly specify it. - * <p>By default this is set to "UTF-8". - */ - public void setCharset(Charset charset) { - this.charset = charset; + applyDefaultCharset(); } - /** - * Set the character set to use when writing multipart data to encode file - * names. Encoding is based on the encoded-word syntax defined in RFC 2047 - * and relies on {@code MimeUtility} from "javax.mail". - * <p>If not set file names will be encoded as US-ASCII. - * @param multipartCharset the charset to use - * @since 4.1.1 - * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a> - */ - public void setMultipartCharset(Charset multipartCharset) { - this.multipartCharset = multipartCharset; - } /** * Set the list of {@link MediaType} objects supported by this converter. @@ -163,6 +144,49 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue this.partConverters.add(partConverter); } + /** + * Set the default character set to use for reading and writing form data when + * the request or response Content-Type header does not explicitly specify it. + * <p>By default this is set to "UTF-8". As of 4.3, it will also be used as + * the default charset for the conversion of text bodies in a multipart request. + * In contrast to this, {@link #setMultipartCharset} only affects the encoding of + * <i>file names</i> in a multipart request according to the encoded-word syntax. + */ + public void setCharset(Charset charset) { + if (charset != this.charset) { + this.charset = (charset != null ? charset : DEFAULT_CHARSET); + applyDefaultCharset(); + } + } + + /** + * Apply the configured charset as a default to registered part converters. + */ + private void applyDefaultCharset() { + for (HttpMessageConverter<?> candidate : this.partConverters) { + if (candidate instanceof AbstractHttpMessageConverter) { + AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate; + // Only override default charset if the converter operates with a charset to begin with... + if (converter.getDefaultCharset() != null) { + converter.setDefaultCharset(this.charset); + } + } + } + } + + /** + * Set the character set to use when writing multipart data to encode file + * names. Encoding is based on the encoded-word syntax defined in RFC 2047 + * and relies on {@code MimeUtility} from "javax.mail". + * <p>If not set file names will be encoded as US-ASCII. + * @param multipartCharset the charset to use + * @since 4.1.1 + * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a> + */ + public void setMultipartCharset(Charset multipartCharset) { + this.multipartCharset = multipartCharset; + } + @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { @@ -202,7 +226,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { MediaType contentType = inputMessage.getHeaders().getContentType(); - Charset charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset); + Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset); String body = StreamUtils.copyToString(inputMessage.getBody(), charset); String[] pairs = StringUtils.tokenizeToStringArray(body, "&"); @@ -255,7 +279,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue Charset charset; if (contentType != null) { outputMessage.getHeaders().setContentType(contentType); - charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset); + charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset); } else { outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); diff --git a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java index 5fab4fec..2e01e5eb 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java @@ -71,7 +71,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve * @param defaultCharset the default charset */ public ObjectToStringHttpMessageConverter(ConversionService conversionService, Charset defaultCharset) { - super(new MediaType("text", "plain", defaultCharset)); + super(defaultCharset, MediaType.TEXT_PLAIN); Assert.notNull(conversionService, "ConversionService is required"); this.conversionService = conversionService; diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index cc8da360..93bd3a3a 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.http.converter; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import javax.activation.FileTypeMap; @@ -33,12 +34,14 @@ import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** - * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}. + * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources} + * and supports byte range requests. * * <p>By default, this converter can read all media types. The Java Activation Framework (JAF) - * if available - is used to determine the {@code Content-Type} of written resources. * If JAF is not available, {@code application/octet-stream} is used. * + * * @author Arjen Poutsma * @author Juergen Hoeller * @author Kazuki Shimizu @@ -64,7 +67,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - if (InputStreamResource.class == clazz){ + if (InputStreamResource.class == clazz) { return new InputStreamResource(inputMessage.getBody()); } else if (clazz.isAssignableFrom(ByteArrayResource.class)) { @@ -90,25 +93,42 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R protected Long getContentLength(Resource resource, MediaType contentType) throws IOException { // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! - return (InputStreamResource.class == resource.getClass() ? null : resource.contentLength()); + if (InputStreamResource.class == resource.getClass()) { + return null; + } + long contentLength = resource.contentLength(); + return (contentLength < 0 ? null : contentLength); } @Override protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { - InputStream in = resource.getInputStream(); + writeContent(resource, outputMessage); + } + + protected void writeContent(Resource resource, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { try { - StreamUtils.copy(in, outputMessage.getBody()); - } - finally { + InputStream in = resource.getInputStream(); try { - in.close(); + StreamUtils.copy(in, outputMessage.getBody()); + } + catch (NullPointerException ex) { + // ignore, see SPR-13620 } - catch (IOException ex) { + finally { + try { + in.close(); + } + catch (Throwable ex) { + // ignore, see SPR-12999 + } } } - outputMessage.getBody().flush(); + catch (FileNotFoundException ex) { + // ignore, see SPR-12999 + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java new file mode 100644 index 00000000..5c12092e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +import org.springframework.core.io.support.ResourceRegion; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StreamUtils; + +/** + * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion}, + * or Collections of {@link ResourceRegion ResourceRegions}. + * + * @author Brian Clozel + * @since 4.3 + */ +public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { + + public ResourceRegionHttpMessageConverter() { + super(MediaType.ALL); + } + + + @Override + protected boolean supports(Class<?> clazz) { + // should not be called as we override canRead/canWrite + return false; + } + + @Override + public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) { + return false; + } + + @Override + public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + return null; + } + + @Override + protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + return null; + } + + @Override + public boolean canWrite(Class<?> clazz, MediaType mediaType) { + return canWrite(clazz, null, mediaType); + } + + @Override + public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) { + if (!(type instanceof ParameterizedType)) { + return ResourceRegion.class.isAssignableFrom((Class) type); + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!(parameterizedType.getRawType() instanceof Class)) { + return false; + } + Class<?> rawType = (Class<?>) parameterizedType.getRawType(); + if (!(Collection.class.isAssignableFrom(rawType))) { + return false; + } + if (parameterizedType.getActualTypeArguments().length != 1) { + return false; + } + Type typeArgument = parameterizedType.getActualTypeArguments()[0]; + if (!(typeArgument instanceof Class)) { + return false; + } + Class<?> typeArgumentClass = (Class<?>) typeArgument; + return typeArgumentClass.isAssignableFrom(ResourceRegion.class); + } + + @Override + @SuppressWarnings("unchecked") + protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + if (object instanceof ResourceRegion) { + writeResourceRegion((ResourceRegion) object, outputMessage); + } + else { + Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object; + if(regions.size() == 1) { + writeResourceRegion(regions.iterator().next(), outputMessage); + } + else { + writeResourceRegionCollection((Collection<ResourceRegion>) object, outputMessage); + } + } + } + + protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException { + Assert.notNull(region, "ResourceRegion must not be null"); + HttpHeaders responseHeaders = outputMessage.getHeaders(); + long start = region.getPosition(); + long end = start + region.getCount() - 1; + Long resourceLength = region.getResource().contentLength(); + end = Math.min(end, resourceLength - 1); + long rangeLength = end - start + 1; + responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + resourceLength); + responseHeaders.setContentLength(rangeLength); + InputStream in = region.getResource().getInputStream(); + try { + StreamUtils.copyRange(in, outputMessage.getBody(), start, end); + } + finally { + try { + in.close(); + } + catch (IOException ex) { + // ignore + } + } + } + + private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions, + HttpOutputMessage outputMessage) throws IOException { + + Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null"); + HttpHeaders responseHeaders = outputMessage.getHeaders(); + MediaType contentType = responseHeaders.getContentType(); + String boundaryString = MimeTypeUtils.generateMultipartBoundaryString(); + responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString); + OutputStream out = outputMessage.getBody(); + for (ResourceRegion region : resourceRegions) { + long start = region.getPosition(); + long end = start + region.getCount() - 1; + InputStream in = region.getResource().getInputStream(); + // Writing MIME header. + println(out); + print(out, "--" + boundaryString); + println(out); + if (contentType != null) { + print(out, "Content-Type: " + contentType.toString()); + println(out); + } + Long resourceLength = region.getResource().contentLength(); + end = Math.min(end, resourceLength - 1); + print(out, "Content-Range: bytes " + start + "-" + end + "/" + resourceLength); + println(out); + println(out); + // Printing content + StreamUtils.copyRange(in, out, start, end); + } + println(out); + print(out, "--" + boundaryString + "--"); + } + + + + private static void println(OutputStream os) throws IOException { + os.write('\r'); + os.write('\n'); + } + + private static void print(OutputStream os, String buf) throws IOException { + os.write(buf.getBytes("US-ASCII")); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 15c2693c..dc150054 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.util.StreamUtils; * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 3.0 */ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> { @@ -42,8 +43,6 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); - private final Charset defaultCharset; - private final List<Charset> availableCharsets; private boolean writeAcceptCharset = true; @@ -62,8 +61,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str * type does not specify one. */ public StringHttpMessageConverter(Charset defaultCharset) { - super(new MediaType("text", "plain", defaultCharset), MediaType.ALL); - this.defaultCharset = defaultCharset; + super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL); this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values()); } @@ -121,11 +119,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str } private Charset getContentTypeCharset(MediaType contentType) { - if (contentType != null && contentType.getCharSet() != null) { - return contentType.getCharSet(); + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); } else { - return this.defaultCharset; + return getDefaultCharset(); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java index 834fc820..9daa030f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java @@ -66,7 +66,7 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed> e WireFeedInput feedInput = new WireFeedInput(); MediaType contentType = inputMessage.getHeaders().getContentType(); Charset charset = - (contentType != null && contentType.getCharSet() != null? contentType.getCharSet() : DEFAULT_CHARSET); + (contentType != null && contentType.getCharset() != null? contentType.getCharset() : DEFAULT_CHARSET); try { Reader reader = new InputStreamReader(inputMessage.getBody(), charset); return (T) feedInput.build(reader); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index f29dc806..2d3dd80e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -17,22 +17,25 @@ package org.springframework.http.converter.json; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.nio.charset.Charset; import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.ser.FilterProvider; import com.fasterxml.jackson.databind.type.TypeFactory; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -41,14 +44,13 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.TypeUtils; /** * Abstract base class for Jackson based and content type independent * {@link HttpMessageConverter} implementations. * - * <p>Compatible with Jackson 2.1 to 2.6. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Arjen Poutsma * @author Keith Donald @@ -61,14 +63,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); - // Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference - private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class, - "canDeserialize", JavaType.class, AtomicReference.class); - - // Check for Jackson 2.6+ for support of generic type aware serialization of polymorphic collections - private static final boolean jackson26Available = ClassUtils.hasMethod(ObjectMapper.class, - "setDefaultPrettyPrinter", PrettyPrinter.class); - protected ObjectMapper objectMapper; @@ -77,16 +71,19 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; + setDefaultCharset(DEFAULT_CHARSET); } protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) { super(supportedMediaType); this.objectMapper = objectMapper; + setDefaultCharset(DEFAULT_CHARSET); } protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) { super(supportedMediaTypes); this.objectMapper = objectMapper; + setDefaultCharset(DEFAULT_CHARSET); } @@ -146,23 +143,14 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return false; } JavaType javaType = getJavaType(type, contextClass); - if (!jackson23Available || !logger.isWarnEnabled()) { + if (!logger.isWarnEnabled()) { return this.objectMapper.canDeserialize(javaType); } AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); if (this.objectMapper.canDeserialize(javaType, causeRef)) { return true; } - Throwable cause = causeRef.get(); - if (cause != null) { - String msg = "Failed to evaluate Jackson deserialization for type " + javaType; - if (logger.isDebugEnabled()) { - logger.warn(msg, cause); - } - else { - logger.warn(msg + ": " + cause); - } - } + logWarningIfNecessary(javaType, causeRef.get()); return false; } @@ -171,16 +159,29 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener if (!canWrite(mediaType)) { return false; } - if (!jackson23Available || !logger.isWarnEnabled()) { + if (!logger.isWarnEnabled()) { return this.objectMapper.canSerialize(clazz); } AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); if (this.objectMapper.canSerialize(clazz, causeRef)) { return true; } - Throwable cause = causeRef.get(); - if (cause != null) { - String msg = "Failed to evaluate Jackson serialization for type [" + clazz + "]"; + logWarningIfNecessary(clazz, causeRef.get()); + return false; + } + + /** + * Determine whether to log the given exception coming from a + * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check. + * @param type the class that Jackson tested for (de-)serializability + * @param cause the Jackson-thrown exception to evaluate + * (typically a {@link JsonMappingException}) + * @since 4.3 + */ + protected void logWarningIfNecessary(Type type, Throwable cause) { + if (cause != null && !(cause instanceof JsonMappingException && cause.getMessage().startsWith("Can not find"))) { + String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + + "serialization for type [" + type + "]"; if (logger.isDebugEnabled()) { logger.warn(msg, cause); } @@ -188,7 +189,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener logger.warn(msg + ": " + cause); } } - return false; } @Override @@ -213,13 +213,12 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return readJavaType(javaType, inputMessage); } - @SuppressWarnings("deprecation") private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { try { if (inputMessage instanceof MappingJacksonInputMessage) { Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView(); if (deserializationView != null) { - return this.objectMapper.readerWithView(deserializationView).withType(javaType). + return this.objectMapper.readerWithView(deserializationView).forType(javaType). readValue(inputMessage.getBody()); } } @@ -231,7 +230,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } @Override - @SuppressWarnings("deprecation") protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -250,7 +248,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener serializationView = container.getSerializationView(); filters = container.getFilters(); } - if (jackson26Available && type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { + if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { javaType = getJavaType(type, null); } ObjectWriter objectWriter; @@ -264,7 +262,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener objectWriter = this.objectMapper.writer(); } if (javaType != null && javaType.isContainerType()) { - objectWriter = objectWriter.withType(javaType); + objectWriter = objectWriter.forType(javaType); } objectWriter.writeValue(generator, value); @@ -313,10 +311,62 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener * @return the Jackson JavaType */ protected JavaType getJavaType(Type type, Class<?> contextClass) { - TypeFactory tf = this.objectMapper.getTypeFactory(); - // Conditional call because Jackson 2.7 does not support null contextClass anymore - // TypeVariable resolution will not work with Jackson 2.7, see SPR-13853 for more details - return (contextClass != null ? tf.constructType(type, contextClass) : tf.constructType(type)); + TypeFactory typeFactory = this.objectMapper.getTypeFactory(); + if (contextClass != null) { + ResolvableType resolvedType = ResolvableType.forType(type); + if (type instanceof TypeVariable) { + ResolvableType resolvedTypeVariable = resolveVariable( + (TypeVariable<?>) type, ResolvableType.forClass(contextClass)); + if (resolvedTypeVariable != ResolvableType.NONE) { + return typeFactory.constructType(resolvedTypeVariable.resolve()); + } + } + else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length]; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + if (typeArgument instanceof TypeVariable) { + ResolvableType resolvedTypeArgument = resolveVariable( + (TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass)); + if (resolvedTypeArgument != ResolvableType.NONE) { + generics[i] = resolvedTypeArgument.resolve(); + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + return typeFactory.constructType(ResolvableType. + forClassWithGenerics(resolvedType.getRawClass(), generics).getType()); + } + } + return typeFactory.constructType(type); + } + + private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) { + ResolvableType resolvedType; + if (contextType.hasGenerics()) { + resolvedType = ResolvableType.forType(typeVariable, contextType); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + resolvedType = resolveVariable(typeVariable, contextType.getSuperType()); + if (resolvedType.resolve() != null) { + return resolvedType; + } + for (ResolvableType ifc : contextType.getInterfaces()) { + resolvedType = resolveVariable(typeVariable, ifc); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + return ResolvableType.NONE; } /** @@ -325,8 +375,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener * @return the JSON encoding to use (never {@code null}) */ protected JsonEncoding getJsonEncoding(MediaType contentType) { - if (contentType != null && contentType.getCharSet() != null) { - Charset charset = contentType.getCharSet(); + if (contentType != null && contentType.getCharset() != null) { + Charset charset = contentType.getCharset(); for (JsonEncoding encoding : JsonEncoding.values()) { if (charset.name().equals(encoding.getJavaName())) { return encoding; diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index 38f7f432..d82bd28d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -68,7 +68,8 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte * Construct a new {@code GsonHttpMessageConverter}. */ public GsonHttpMessageConverter() { - super(MediaType.APPLICATION_JSON_UTF8, new MediaType("application", "*+json", DEFAULT_CHARSET)); + super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + this.setDefaultCharset(DEFAULT_CHARSET); } @@ -177,10 +178,10 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte } private Charset getCharset(HttpHeaders headers) { - if (headers == null || headers.getContentType() == null || headers.getContentType().getCharSet() == null) { + if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) { return DEFAULT_CHARSET; } - return headers.getContentType().getCharSet(); + return headers.getContentType().getCharset(); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index b2ee1e5b..046a3b1e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,8 @@ import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; +import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import org.springframework.beans.BeanUtils; @@ -73,9 +75,10 @@ import org.springframework.util.StringUtils; * <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>: support for other Java 8 types like {@link java.util.Optional}</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>: support for Java 8 Date & Time API types</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-joda">jackson-datatype-joda</a>: support for Joda-Time types</li> + * <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>: support for Kotlin classes and data classes</li> * </ul> * - * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Sebastien Deleuze * @author Juergen Hoeller @@ -127,6 +130,8 @@ public class Jackson2ObjectMapperBuilder { private ApplicationContext applicationContext; + private Boolean defaultUseWrapper; + /** * If set to {@code true}, an {@link XmlMapper} will be created using its @@ -279,8 +284,7 @@ public class Jackson2ObjectMapperBuilder { /** * Configure custom serializers. Each serializer is registered for the type - * returned by {@link JsonSerializer#handledType()}, which must not be - * {@code null}. + * returned by {@link JsonSerializer#handledType()}, which must not be {@code null}. * @see #serializersByType(Map) */ public Jackson2ObjectMapperBuilder serializers(JsonSerializer<?>... serializers) { @@ -320,6 +324,25 @@ public class Jackson2ObjectMapperBuilder { } /** + * Configure custom deserializers. Each deserializer is registered for the type + * returned by {@link JsonDeserializer#handledType()}, which must not be {@code null}. + * @since 4.3 + * @see #deserializersByType(Map) + */ + public Jackson2ObjectMapperBuilder deserializers(JsonDeserializer<?>... deserializers) { + if (deserializers != null) { + for (JsonDeserializer<?> deserializer : deserializers) { + Class<?> handledType = deserializer.handledType(); + if (handledType == null || handledType == Object.class) { + throw new IllegalArgumentException("Unknown handled type in " + deserializer.getClass().getName()); + } + this.deserializers.put(deserializer.handledType(), deserializer); + } + } + return this; + } + + /** * Configure a custom deserializer for the given type. * @since 4.1.2 */ @@ -393,6 +416,16 @@ public class Jackson2ObjectMapperBuilder { } /** + * Define if a wrapper will be used for indexed (List, array) properties or not by + * default (only applies to {@link XmlMapper}). + * @since 4.3 + */ + public Jackson2ObjectMapperBuilder defaultUseWrapper(boolean defaultUseWrapper) { + this.defaultUseWrapper = defaultUseWrapper; + return this; + } + + /** * Specify features to enable. * @see com.fasterxml.jackson.core.JsonParser.Feature * @see com.fasterxml.jackson.core.JsonGenerator.Feature @@ -547,7 +580,9 @@ public class Jackson2ObjectMapperBuilder { public <T extends ObjectMapper> T build() { ObjectMapper mapper; if (this.createXmlMapper) { - mapper = new XmlObjectMapperInitializer().create(); + mapper = (this.defaultUseWrapper != null ? + new XmlObjectMapperInitializer().create(this.defaultUseWrapper) : + new XmlObjectMapperInitializer().create()); } else { mapper = new ObjectMapper(); @@ -561,7 +596,6 @@ public class Jackson2ObjectMapperBuilder { * settings. This can be applied to any number of {@code ObjectMappers}. * @param objectMapper the ObjectMapper to configure */ - @SuppressWarnings("deprecation") public void configure(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); @@ -609,13 +643,11 @@ public class Jackson2ObjectMapperBuilder { } if (this.filters != null) { - // Deprecated as of Jackson 2.6, but just in favor of a fluent variant. - objectMapper.setFilters(this.filters); + objectMapper.setFilterProvider(this.filters); } for (Class<?> target : this.mixIns.keySet()) { - // Deprecated as of Jackson 2.5, but just in favor of a fluent variant. - objectMapper.addMixInAnnotations(target, this.mixIns.get(target)); + objectMapper.addMixIn(target, this.mixIns.get(target)); } if (!this.serializers.isEmpty() || !this.deserializers.isEmpty()) { @@ -693,7 +725,7 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> jdk7Module = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk7.Jdk7Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jdk7Module)); + objectMapper.registerModule(BeanUtils.instantiateClass(jdk7Module)); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk7 not available @@ -705,7 +737,7 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> jdk8Module = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jdk8Module)); + objectMapper.registerModule(BeanUtils.instantiateClass(jdk8Module)); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk8 not available @@ -717,18 +749,10 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> javaTimeModule = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(javaTimeModule)); + objectMapper.registerModule(BeanUtils.instantiateClass(javaTimeModule)); } catch (ClassNotFoundException ex) { - // jackson-datatype-jsr310 not available or older than 2.6 - try { - Class<? extends Module> jsr310Module = (Class<? extends Module>) - ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JSR310Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jsr310Module)); - } - catch (ClassNotFoundException ex2) { - // OK, jackson-datatype-jsr310 not available at all... - } + // jackson-datatype-jsr310 not available } } @@ -737,12 +761,24 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> jodaModule = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jodaModule)); + objectMapper.registerModule(BeanUtils.instantiateClass(jodaModule)); } catch (ClassNotFoundException ex) { // jackson-datatype-joda not available } } + + // Kotlin present? + if (ClassUtils.isPresent("kotlin.Unit", this.moduleClassLoader)) { + try { + Class<? extends Module> kotlinModule = (Class<? extends Module>) + ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader); + objectMapper.registerModule(BeanUtils.instantiateClass(kotlinModule)); + } + catch (ClassNotFoundException ex) { + // jackson-module-kotlin not available + } + } } @@ -768,11 +804,21 @@ public class Jackson2ObjectMapperBuilder { private static class XmlObjectMapperInitializer { public ObjectMapper create() { + return new XmlMapper(xmlInputFactory()); + } + + public ObjectMapper create(boolean defaultUseWrapper) { + JacksonXmlModule module = new JacksonXmlModule(); + module.setDefaultUseWrapper(defaultUseWrapper); + return new XmlMapper(new XmlFactory(xmlInputFactory()), module); + } + + private static XMLInputFactory xmlInputFactory() { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); inputFactory.setXMLResolver(NO_OP_XML_RESOLVER); - return new XmlMapper(inputFactory); + return inputFactory; } private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() { diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java index f17016e9..d945a7fb 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,6 +116,7 @@ import org.springframework.context.ApplicationContextAware; * <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>: support for other Java 8 types like {@link java.util.Optional}</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>: support for Java 8 Date & Time API types</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-joda">jackson-datatype-joda</a>: support for Joda-Time types</li> + * <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>: support for Kotlin classes and data classes</li> * </ul> * * <p>In case you want to configure Jackson's {@link ObjectMapper} with a custom {@link Module}, @@ -127,7 +128,7 @@ import org.springframework.context.ApplicationContextAware; * </bean * </pre> * - * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author <a href="mailto:dmitry.katsubo@gmail.com">Dmitry Katsubo</a> * @author Rossen Stoyanchev @@ -255,8 +256,7 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper /** * Configure custom serializers. Each serializer is registered for the type - * returned by {@link JsonSerializer#handledType()}, which must not be - * {@code null}. + * returned by {@link JsonSerializer#handledType()}, which must not be {@code null}. * @see #setSerializersByType(Map) */ public void setSerializers(JsonSerializer<?>... serializers) { @@ -272,6 +272,16 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper } /** + * Configure custom deserializers. Each deserializer is registered for the type + * returned by {@link JsonDeserializer#handledType()}, which must not be {@code null}. + * @since 4.3 + * @see #setDeserializersByType(Map) + */ + public void setDeserializers(JsonDeserializer<?>... deserializers) { + this.builder.deserializers(deserializers); + } + + /** * Configure custom deserializers for the given types. */ public void setDeserializersByType(Map<Class<?>, JsonDeserializer<?>> deserializers) { @@ -325,6 +335,15 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper } /** + * Define if a wrapper will be used for indexed (List, array) properties or not by + * default (only applies to {@link XmlMapper}). + * @since 4.3 + */ + public void setDefaultUseWrapper(boolean defaultUseWrapper) { + this.builder.defaultUseWrapper(defaultUseWrapper); + } + + /** * Specify features to enable. * @see com.fasterxml.jackson.core.JsonParser.Feature * @see com.fasterxml.jackson.core.JsonGenerator.Feature diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java index e2ef12b9..4832ec0b 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java @@ -35,7 +35,7 @@ import org.springframework.http.MediaType; * * <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. * - * <p>Compatible with Jackson 2.1 to 2.6. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Arjen Poutsma * @author Keith Donald @@ -63,8 +63,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMes * @see Jackson2ObjectMapperBuilder#json() */ public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) { - super(objectMapper, MediaType.APPLICATION_JSON_UTF8, - new MediaType("application", "*+json", DEFAULT_CHARSET)); + super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java b/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java index f208c32a..ad4874c1 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/SpringHandlerInstantiator.java @@ -16,16 +16,22 @@ package org.springframework.http.converter.json; +import com.fasterxml.jackson.annotation.ObjectIdGenerator; +import com.fasterxml.jackson.annotation.ObjectIdResolver; import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.introspect.Annotated; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter; +import com.fasterxml.jackson.databind.util.Converter; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; @@ -36,6 +42,11 @@ import org.springframework.util.Assert; * {@link KeyDeserializer}, {@link TypeResolverBuilder}, {@link TypeIdResolver}) * beans with autowiring against a Spring {@link ApplicationContext}. * + * <p>As of Spring 4.3, this overrides all factory methods in {@link HandlerInstantiator}, + * including non-abstract ones and recently introduced ones from Jackson 2.4 and 2.5: + * for {@link ValueInstantiator}, {@link ObjectIdGenerator}, {@link ObjectIdResolver}, + * {@link PropertyNamingStrategy}, {@link Converter}, {@link VirtualBeanPropertyWriter}. + * * @author Sebastien Deleuze * @author Juergen Hoeller * @since 4.1.3 @@ -83,4 +94,40 @@ public class SpringHandlerInstantiator extends HandlerInstantiator { return (TypeIdResolver) this.beanFactory.createBean(implClass); } + /** @since 4.3 */ + @Override + public ValueInstantiator valueInstantiatorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) { + return (ValueInstantiator) this.beanFactory.createBean(implClass); + } + + /** @since 4.3 */ + @Override + public ObjectIdGenerator<?> objectIdGeneratorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) { + return (ObjectIdGenerator<?>) this.beanFactory.createBean(implClass); + } + + /** @since 4.3 */ + @Override + public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) { + return (ObjectIdResolver) this.beanFactory.createBean(implClass); + } + + /** @since 4.3 */ + @Override + public PropertyNamingStrategy namingStrategyInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) { + return (PropertyNamingStrategy) this.beanFactory.createBean(implClass); + } + + /** @since 4.3 */ + @Override + public Converter<?, ?> converterInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) { + return (Converter<?, ?>) this.beanFactory.createBean(implClass); + } + + /** @since 4.3 */ + @Override + public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig<?> config, Class<?> implClass) { + return (VirtualBeanPropertyWriter) this.beanFactory.createBean(implClass); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java index 388b5533..9f023882 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -28,6 +28,7 @@ import com.google.protobuf.Message; import com.google.protobuf.TextFormat; import com.googlecode.protobuf.format.HtmlFormat; import com.googlecode.protobuf.format.JsonFormat; +import com.googlecode.protobuf.format.ProtobufFormatter; import com.googlecode.protobuf.format.XmlFormat; import org.springframework.http.HttpInputMessage; @@ -38,7 +39,6 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.FileCopyUtils; - /** * An {@code HttpMessageConverter} that reads and writes {@link com.google.protobuf.Message}s * using <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>. @@ -48,8 +48,7 @@ import org.springframework.util.FileCopyUtils; * * <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary. * - * <p>Requires Protobuf 2.5/2.6 and Protobuf Java Format 1.2. - * (Note: Does not work with later Protobuf Java Format versions in Spring 4.2 yet.) + * <p>Requires Protobuf 2.6 and Protobuf Java Format 1.4, as of Spring 4.3. * * @author Alex Antonov * @author Brian Clozel @@ -67,6 +66,13 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message"; + private static final ProtobufFormatter JSON_FORMAT = new JsonFormat(); + + private static final ProtobufFormatter XML_FORMAT = new XmlFormat(); + + private static final ProtobufFormatter HTML_FORMAT = new HtmlFormat(); + + private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<Class<?>, Method>(); private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); @@ -109,7 +115,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M if (contentType == null) { contentType = PROTOBUF; } - Charset charset = contentType.getCharSet(); + Charset charset = contentType.getCharset(); if (charset == null) { charset = DEFAULT_CHARSET; } @@ -121,12 +127,10 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M TextFormat.merge(reader, this.extensionRegistry, builder); } else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) { - InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset); - JsonFormat.merge(reader, this.extensionRegistry, builder); + JSON_FORMAT.merge(inputMessage.getBody(), charset, this.extensionRegistry, builder); } else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset); - XmlFormat.merge(reader, this.extensionRegistry, builder); + XML_FORMAT.merge(inputMessage.getBody(), charset, this.extensionRegistry, builder); } else { builder.mergeFrom(inputMessage.getBody(), this.extensionRegistry); @@ -155,7 +159,7 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M if (contentType == null) { contentType = getDefaultContentType(message); } - Charset charset = contentType.getCharSet(); + Charset charset = contentType.getCharset(); if (charset == null) { charset = DEFAULT_CHARSET; } @@ -166,19 +170,13 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M outputStreamWriter.flush(); } else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) { - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset); - JsonFormat.print(message, outputStreamWriter); - outputStreamWriter.flush(); + JSON_FORMAT.print(message, outputMessage.getBody(), charset); } else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset); - XmlFormat.print(message, outputStreamWriter); - outputStreamWriter.flush(); + XML_FORMAT.print(message, outputMessage.getBody(), charset); } else if (MediaType.TEXT_HTML.isCompatibleWith(contentType)) { - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset); - HtmlFormat.print(message, outputStreamWriter); - outputStreamWriter.flush(); + HTML_FORMAT.print(message, outputMessage.getBody(), charset); } else if (PROTOBUF.isCompatibleWith(contentType)) { setProtoHeader(outputMessage, message); diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java index 53773a48..5be9e392 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java @@ -51,7 +51,7 @@ import org.springframework.util.ClassUtils; * HttpMessageConverter} that can read and write XML using JAXB2. * * <p>This converter can read classes annotated with {@link XmlRootElement} and - * {@link XmlType}, and write classes annotated with with {@link XmlRootElement}, + * {@link XmlType}, and write classes annotated with {@link XmlRootElement}, * or subclasses thereof. * * <p>Note that if using Spring's Marshaller/Unmarshaller abstractions from the @@ -195,8 +195,8 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa } private void setCharset(MediaType contentType, Marshaller marshaller) throws PropertyException { - if (contentType != null && contentType.getCharSet() != null) { - marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharSet().name()); + if (contentType != null && contentType.getCharset() != null) { + marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharset().name()); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java index 03084759..39eb13ef 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java @@ -35,7 +35,7 @@ import org.springframework.util.Assert; * * <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. * - * <p>Compatible with Jackson 2.1 to 2.6. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Sebastien Deleuze * @since 4.1 @@ -57,9 +57,9 @@ public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2Http * @see Jackson2ObjectMapperBuilder#xml() */ public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { - super(objectMapper, new MediaType("application", "xml", DEFAULT_CHARSET), - new MediaType("text", "xml", DEFAULT_CHARSET), - new MediaType("application", "*+xml", DEFAULT_CHARSET)); + super(objectMapper, new MediaType("application", "xml"), + new MediaType("text", "xml"), + new MediaType("application", "*+xml")); Assert.isAssignable(XmlMapper.class, objectMapper.getClass()); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java index e9875107..8b411259 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -175,9 +175,8 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe } catch (NullPointerException ex) { if (!isSupportDtd()) { - throw new HttpMessageNotReadableException("NPE while unmarshalling. " + - "This can happen on JDK 1.6 due to the presence of DTD " + - "declarations, which are disabled.", ex); + throw new HttpMessageNotReadableException("NPE while unmarshalling: " + + "This can happen due to the presence of DTD declarations which are disabled.", ex); } throw ex; } @@ -191,14 +190,14 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe private SAXSource readSAXSource(InputStream body) throws IOException { try { - XMLReader reader = XMLReaderFactory.createXMLReader(); - reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); - reader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); + xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); if (!isProcessExternalEntities()) { - reader.setEntityResolver(NO_OP_ENTITY_RESOLVER); + xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER); } byte[] bytes = StreamUtils.copyToByteArray(body); - return new SAXSource(reader, new InputSource(new ByteArrayInputStream(bytes))); + return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream(bytes))); } catch (SAXException ex) { throw new HttpMessageNotReadableException("Could not parse document: " + ex.getMessage(), ex); diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index 4a8f342a..660044b8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -125,7 +125,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { this.headers.setContentType(contentType); } } - if (contentType != null && contentType.getCharSet() == null) { + if (contentType != null && contentType.getCharset() == null) { String requestEncoding = this.servletRequest.getCharacterEncoding(); if (StringUtils.hasLength(requestEncoding)) { Charset charSet = Charset.forName(requestEncoding); diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java index 1aebe518..5944297f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public void setStatusCode(HttpStatus status) { + Assert.notNull(status, "HttpStatus must not be null"); this.servletResponse.setStatus(status.value()); } @@ -114,8 +115,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse { this.servletResponse.setContentType(this.headers.getContentType().toString()); } if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null && - this.headers.getContentType().getCharSet() != null) { - this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name()); + this.headers.getContentType().getCharset() != null) { + this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name()); } this.headersWritten = true; } diff --git a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java index 7b0924ba..7f78a822 100644 --- a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java +++ b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import org.springframework.context.i18n.LocaleContext; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.remoting.support.RemoteInvocationResult; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -69,6 +70,21 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); + + private static Class<?> abstractHttpClientClass; + + static { + try { + // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3) + abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient", + HttpComponentsHttpInvokerRequestExecutor.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Probably removed from HttpComponents in the meantime... + } + } + + private HttpClient httpClient; private RequestConfig requestConfig; @@ -156,7 +172,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke */ @SuppressWarnings("deprecation") private void setLegacyConnectionTimeout(HttpClient client, int timeout) { - if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { + if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) { client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); } } @@ -198,7 +214,7 @@ public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvoke */ @SuppressWarnings("deprecation") private void setLegacySocketTimeout(HttpClient client, int timeout) { - if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { + if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) { client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout); } } diff --git a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java index bcbc09f9..82bb00b6 100644 --- a/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java +++ b/spring-web/src/main/java/org/springframework/remoting/httpinvoker/HttpInvokerServiceExporter.java @@ -16,6 +16,7 @@ package org.springframework.remoting.httpinvoker; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; @@ -174,7 +175,8 @@ public class HttpInvokerServiceExporter extends RemoteInvocationSerializingExpor HttpServletRequest request, HttpServletResponse response, RemoteInvocationResult result, OutputStream os) throws IOException { - ObjectOutputStream oos = createObjectOutputStream(decorateOutputStream(request, response, os)); + ObjectOutputStream oos = + createObjectOutputStream(new FlushGuardedOutputStream(decorateOutputStream(request, response, os))); try { doWriteRemoteInvocationResult(result, oos); } @@ -200,4 +202,28 @@ public class HttpInvokerServiceExporter extends RemoteInvocationSerializingExpor return os; } + + /** + * Decorate an {@code OutputStream} to guard against {@code flush()} calls, + * which are turned into no-ops. + * + * <p>Because {@link ObjectOutputStream#close()} will in fact flush/drain + * the underlying stream twice, this {@link FilterOutputStream} will + * guard against individual flush calls. Multiple flush calls can lead + * to performance issues, since writes aren't gathered as they should be. + * + * @see <a href="https://jira.spring.io/browse/SPR-14040">SPR-14040</a> + */ + private static class FlushGuardedOutputStream extends FilterOutputStream { + + public FlushGuardedOutputStream(OutputStream out) { + super(out); + } + + @Override + public void flush() throws IOException { + // Do nothing on flush + } + } + } diff --git a/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java b/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java index 08c47e61..8746786f 100644 --- a/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java +++ b/spring-web/src/main/java/org/springframework/remoting/jaxws/SimpleJaxWsServiceExporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,9 @@ import javax.xml.ws.WebServiceProvider; * <p>Note that this exporter will only work if the JAX-WS runtime actually * supports publishing with an address argument, i.e. if the JAX-WS runtime * ships an internal HTTP server. This is the case with the JAX-WS runtime - * that's inclued in Sun's JDK 1.6 but not with the standalone JAX-WS 2.1 RI. + * that's included in Sun's JDK 6 but not with the standalone JAX-WS 2.1 RI. * - * <p>For explicit configuration of JAX-WS endpoints with Sun's JDK 1.6 + * <p>For explicit configuration of JAX-WS endpoints with Sun's JDK 6 * HTTP server, consider using {@link SimpleHttpServerJaxWsServiceExporter}! * * @author Juergen Hoeller diff --git a/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java b/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java index 448bd4ed..d17f8d39 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpSessionRequiredException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,9 @@ import javax.servlet.ServletException; @SuppressWarnings("serial") public class HttpSessionRequiredException extends ServletException { + private String expectedAttribute; + + /** * Create a new HttpSessionRequiredException. * @param msg the detail message @@ -35,4 +38,24 @@ public class HttpSessionRequiredException extends ServletException { super(msg); } + /** + * Create a new HttpSessionRequiredException. + * @param msg the detail message + * @param expectedAttribute the name of the expected session attribute + * @since 4.3 + */ + public HttpSessionRequiredException(String msg, String expectedAttribute) { + super(msg); + this.expectedAttribute = expectedAttribute; + } + + + /** + * Return the name of the expected session attribute, if any. + * @since 4.3 + */ + public String getExpectedAttribute() { + return this.expectedAttribute; + } + } diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java index 3f6efab3..81553b81 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java @@ -93,6 +93,22 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me } /** + * Find a {@code ContentNegotiationStrategy} of the given type. + * @param strategyType the strategy type + * @return the first matching strategy or {@code null}. + * @since 4.3 + */ + @SuppressWarnings("unchecked") + public <T extends ContentNegotiationStrategy> T getStrategy(Class<T> strategyType) { + for (ContentNegotiationStrategy strategy : getStrategies()) { + if (strategyType.isInstance(strategy)) { + return (T) strategy; + } + } + return null; + } + + /** * Register more {@code MediaTypeFileExtensionResolver} instances in addition * to those detected at construction. * @param resolvers the resolvers to add diff --git a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java index 2642853d..89dd23a2 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java @@ -16,13 +16,13 @@ package org.springframework.web.accept; +import java.util.Arrays; import java.util.Collections; import java.util.List; import org.springframework.http.HttpHeaders; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; -import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.context.request.NativeWebRequest; @@ -30,6 +30,7 @@ import org.springframework.web.context.request.NativeWebRequest; * A {@code ContentNegotiationStrategy} that checks the 'Accept' request header. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.2 */ public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { @@ -42,18 +43,20 @@ public class HeaderContentNegotiationStrategy implements ContentNegotiationStrat public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { - String header = request.getHeader(HttpHeaders.ACCEPT); - if (!StringUtils.hasText(header)) { - return Collections.emptyList(); + String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); + if (headerValueArray == null) { + return Collections.<MediaType>emptyList(); } + + List<String> headerValues = Arrays.asList(headerValueArray); try { - List<MediaType> mediaTypes = MediaType.parseMediaTypes(header); + List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues); MediaType.sortBySpecificityAndQuality(mediaTypes); return mediaTypes; } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotAcceptableException( - "Could not parse 'Accept' header [" + header + "]: " + ex.getMessage()); + "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage()); } } diff --git a/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java index 9f0c2672..000b6379 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java @@ -30,12 +30,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.util.UriUtils; import org.springframework.web.util.UrlPathHelper; -import org.springframework.web.util.WebUtils; /** * A {@code ContentNegotiationStrategy} that resolves the file extension in the @@ -51,20 +52,14 @@ import org.springframework.web.util.WebUtils; * @author Rossen Stoyanchev * @since 3.2 */ -public class PathExtensionContentNegotiationStrategy - extends AbstractMappingContentNegotiationStrategy { - - private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class); +public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap", PathExtensionContentNegotiationStrategy.class.getClassLoader()); - private static final UrlPathHelper PATH_HELPER = new UrlPathHelper(); - - static { - PATH_HELPER.setUrlDecode(false); - } + private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class); + private UrlPathHelper urlPathHelper = new UrlPathHelper(); private boolean useJaf = true; @@ -72,21 +67,31 @@ public class PathExtensionContentNegotiationStrategy /** + * Create an instance without any mappings to start with. Mappings may be added + * later on if any extensions are resolved through the Java Activation framework. + */ + public PathExtensionContentNegotiationStrategy() { + this(null); + } + + /** * Create an instance with the given map of file extensions and media types. */ public PathExtensionContentNegotiationStrategy(Map<String, MediaType> mediaTypes) { super(mediaTypes); + this.urlPathHelper.setUrlDecode(false); } + /** - * Create an instance without any mappings to start with. Mappings may be added - * later on if any extensions are resolved through the Java Activation framework. + * Configure a {@code UrlPathHelper} to use in {@link #getMediaTypeKey} + * in order to derive the lookup path for a target request URL path. + * @since 4.2.8 */ - public PathExtensionContentNegotiationStrategy() { - super(null); + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + this.urlPathHelper = urlPathHelper; } - /** * Whether to use the Java Activation Framework to look up file extensions. * <p>By default this is set to "true" but depends on JAF being present. @@ -112,10 +117,9 @@ public class PathExtensionContentNegotiationStrategy logger.warn("An HttpServletRequest is required to determine the media type key"); return null; } - String path = PATH_HELPER.getLookupPathForRequest(request); - String filename = WebUtils.extractFullFilenameFromUrlPath(path); - String extension = StringUtils.getFilenameExtension(filename); - return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null; + String path = this.urlPathHelper.getLookupPathForRequest(request); + String extension = UriUtils.extractFileExtension(path); + return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ENGLISH) : null); } @Override @@ -123,7 +127,7 @@ public class PathExtensionContentNegotiationStrategy throws HttpMediaTypeNotAcceptableException { if (this.useJaf && JAF_PRESENT) { - MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + extension); + MediaType mediaType = ActivationMediaTypeFactory.getMediaType("file." + extension); if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { return mediaType; } @@ -134,11 +138,37 @@ public class PathExtensionContentNegotiationStrategy throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes()); } + /** + * A public method exposing the knowledge of the path extension strategy to + * resolve file extensions to a MediaType in this case for a given + * {@link Resource}. The method first looks up any explicitly registered + * file extensions first and then falls back on JAF if available. + * @param resource the resource to look up + * @return the MediaType for the extension or {@code null}. + * @since 4.3 + */ + public MediaType getMediaTypeForResource(Resource resource) { + Assert.notNull(resource); + MediaType mediaType = null; + String filename = resource.getFilename(); + String extension = StringUtils.getFilenameExtension(filename); + if (extension != null) { + mediaType = lookupMediaType(extension); + } + if (mediaType == null && JAF_PRESENT) { + mediaType = ActivationMediaTypeFactory.getMediaType(filename); + } + if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { + mediaType = null; + } + return mediaType; + } + /** * Inner class to avoid hard-coded dependency on JAF. */ - private static class JafMediaTypeFactory { + private static class ActivationMediaTypeFactory { private static final FileTypeMap fileTypeMap; diff --git a/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java index 615568b4..10d80b2b 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ServletPathExtensionContentNegotiationStrategy.java @@ -19,6 +19,7 @@ package org.springframework.web.accept; import java.util.Map; import javax.servlet.ServletContext; +import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -82,4 +83,29 @@ public class ServletPathExtensionContentNegotiationStrategy extends PathExtensio return mediaType; } + /** + * Extends the base class + * {@link PathExtensionContentNegotiationStrategy#getMediaTypeForResource} + * with the ability to also look up through the ServletContext. + * @param resource the resource to look up + * @return the MediaType for the extension or {@code null}. + * @since 4.3 + */ + public MediaType getMediaTypeForResource(Resource resource) { + MediaType mediaType = null; + if (this.servletContext != null) { + String mimeType = this.servletContext.getMimeType(resource.getFilename()); + if (StringUtils.hasText(mimeType)) { + mediaType = MediaType.parseMediaType(mimeType); + } + } + if (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { + MediaType superMediaType = super.getMediaTypeForResource(resource); + if (superMediaType != null) { + mediaType = superMediaType; + } + } + return mediaType; + } + } diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java index 77562398..442b9718 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,13 @@ package org.springframework.web.bind; import java.lang.reflect.Array; +import java.util.Collection; import java.util.List; import java.util.Map; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; +import org.springframework.core.CollectionFactory; import org.springframework.validation.DataBinder; import org.springframework.web.multipart.MultipartFile; @@ -40,6 +42,7 @@ import org.springframework.web.multipart.MultipartFile; * * @author Juergen Hoeller * @author Scott Andrews + * @author Brian Clozel * @since 1.2 * @see #registerCustomEditor * @see #setAllowedFields @@ -243,26 +246,41 @@ public class WebDataBinder extends DataBinder { /** * Determine an empty value for the specified field. - * <p>Default implementation returns {@code Boolean.FALSE} - * for boolean fields and an empty array of array types. - * Else, {@code null} is used as default. + * <p>Default implementation returns: + * <ul> + * <li>{@code Boolean.FALSE} for boolean fields + * <li>an empty array for array types + * <li>Collection implementations for Collection types + * <li>Map implementations for Map types + * <li>else, {@code null} is used as default + * </ul> * @param field the name of the field * @param fieldType the type of the field * @return the empty value (for most fields: null) */ protected Object getEmptyValue(String field, Class<?> fieldType) { - if (fieldType != null && boolean.class == fieldType || Boolean.class == fieldType) { - // Special handling of boolean property. - return Boolean.FALSE; - } - else if (fieldType != null && fieldType.isArray()) { - // Special handling of array property. - return Array.newInstance(fieldType.getComponentType(), 0); - } - else { - // Default value: try null. - return null; + if (fieldType != null) { + try { + if (boolean.class == fieldType || Boolean.class == fieldType) { + // Special handling of boolean property. + return Boolean.FALSE; + } + else if (fieldType.isArray()) { + // Special handling of array property. + return Array.newInstance(fieldType.getComponentType(), 0); + } + else if (Collection.class.isAssignableFrom(fieldType)) { + return CollectionFactory.createCollection(fieldType, 0); + } + else if (Map.class.isAssignableFrom(fieldType)) { + return CollectionFactory.createMap(fieldType, 0); + } + } catch (IllegalArgumentException exc) { + return null; + } } + // Default value: try null. + return null; } /** diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java index 65e92007..76aacd0a 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,14 @@ import org.springframework.core.annotation.AliasFor; * * <p>By default, all origins and headers are permitted. * + * <p><b>NOTE:</b> {@code @CrossOrigin} is processed if an appropriate + * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the + * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter} + * pair which are the default in the MVC Java config and the MVC namespace. + * In particular {@code @CrossOrigin} is not supported with the + * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter} + * pair both of which are also deprecated. + * * @author Russell Allen * @author Sebastien Deleuze * @author Sam Brannen diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java new file mode 100644 index 00000000..a437f067 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code DELETE} requests onto specific handler + * methods. + * + * <p>Specifically, {@code @DeleteMapping} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.DELETE)}. + * + * @author Sam Brannen + * @since 4.3 + * @see GetMapping + * @see PostMapping + * @see PutMapping + * @see PatchMapping + * @see RequestMapping + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.DELETE) +public @interface DeleteMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java new file mode 100644 index 00000000..12c8a6cd --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code GET} requests onto specific handler + * methods. + * + * <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}. + * + * <h5>Difference between {@code @GetMapping} & {@code @RequestMapping}</h5> + * <p>{@code @GetMapping} does not support the {@link RequestMapping#consumes consumes} + * attribute of {@code @RequestMapping}. + * + * @author Sam Brannen + * @since 4.3 + * @see PostMapping + * @see PutMapping + * @see DeleteMapping + * @see PatchMapping + * @see RequestMapping + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.GET) +public @interface GetMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java index 39f31413..7d93e660 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ModelAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; import org.springframework.ui.Model; /** @@ -49,6 +50,7 @@ import org.springframework.ui.Model; * access to a {@link Model} argument. * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 2.5 */ @Target({ElementType.PARAMETER, ElementType.METHOD}) @@ -57,13 +59,31 @@ import org.springframework.ui.Model; public @interface ModelAttribute { /** + * Alias for {@link #name}. + */ + @AliasFor("name") + String value() default ""; + + /** * The name of the model attribute to bind to. * <p>The default model attribute name is inferred from the declared * attribute type (i.e. the method parameter type or method return type), * based on the non-qualified class name: * e.g. "orderAddress" for class "mypackage.OrderAddress", * or "orderAddressList" for "List<mypackage.OrderAddress>". + * @since 4.3 */ - String value() default ""; + @AliasFor("value") + String name() default ""; + + /** + * Allows declaring data binding disabled directly on an {@code @ModelAttribute} + * method parameter or on the attribute returned from an {@code @ModelAttribute} + * method, both of which would prevent data binding for that attribute. + * <p>By default this is set to {@code true} in which case data binding applies. + * Set this to {@code false} to disable data binding. + * @since 4.3 + */ + boolean binding() default true; } diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java new file mode 100644 index 00000000..82eee4ac --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PatchMapping.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code PATCH} requests onto specific handler + * methods. + * + * <p>Specifically, {@code @PatchMapping} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PATCH)}. + * + * @author Sam Brannen + * @since 4.3 + * @see GetMapping + * @see PostMapping + * @see PutMapping + * @see DeleteMapping + * @see RequestMapping + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.PATCH) +public @interface PatchMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java new file mode 100644 index 00000000..0bc64bca --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code POST} requests onto specific handler + * methods. + * + * <p>Specifically, {@code @PostMapping} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.POST)}. + * + * @author Sam Brannen + * @since 4.3 + * @see GetMapping + * @see PutMapping + * @see DeleteMapping + * @see PatchMapping + * @see RequestMapping + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.POST) +public @interface PostMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java new file mode 100644 index 00000000..8e9e71f6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping HTTP {@code PUT} requests onto specific handler + * methods. + * + * <p>Specifically, {@code @PutMapping} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @RequestMapping(method = RequestMethod.PUT)}. + * + * @author Sam Brannen + * @since 4.3 + * @see GetMapping + * @see PostMapping + * @see DeleteMapping + * @see PatchMapping + * @see RequestMapping + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.PUT) +public @interface PutMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java new file mode 100644 index 00000000..8a51119e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestAttribute.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to bind a method parameter to a request attribute. + * + * <p>The main motivation is to provide convenient access to request attributes + * from a controller method with an optional/required check and a cast to the + * target method parameter type. + * + * @author Rossen Stoyanchev + * @since 4.3 + * @see RequestMapping + * @see SessionAttribute + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequestAttribute { + + /** + * Alias for {@link #name}. + */ + @AliasFor("name") + String value() default ""; + + /** + * The name of the request attribute to bind to. + * <p>The default name is inferred from the method parameter name. + */ + @AliasFor("value") + String name() default ""; + + /** + * Whether the request attribute is required. + * <p>Defaults to {@code true}, leading to an exception being thrown if + * the attribute is missing. Switch this to {@code false} if you prefer + * a {@code null} or Java 8 {@code java.util.Optional} if the attribute + * doesn't exist. + */ + boolean required() default true; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index c884775c..cda331c4 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,6 +126,12 @@ import org.springframework.core.annotation.AliasFor; * {@link org.springframework.validation.Errors} argument. * Instead a {@link org.springframework.web.bind.MethodArgumentNotValidException} * exception is raised. + * <li>{@link SessionAttribute @SessionAttribute} annotated parameters for access + * to existing, permanent session attributes (e.g. user authentication object) + * as opposed to model attributes temporarily stored in the session as part of + * a controller workflow via {@link SessionAttributes}. + * <li>{@link RequestAttribute @RequestAttribute} annotated parameters for access + * to request attributes. * <li>{@link org.springframework.http.HttpEntity HttpEntity<?>} parameters * (Servlet-only) for access to the Servlet request HTTP headers and contents. * The request stream will be converted to the entity body using @@ -273,8 +279,16 @@ import org.springframework.core.annotation.AliasFor; * @author Arjen Poutsma * @author Sam Brannen * @since 2.5 + * @see GetMapping + * @see PostMapping + * @see PutMapping + * @see DeleteMapping + * @see PatchMapping * @see RequestParam + * @see RequestAttribute + * @see PathVariable * @see ModelAttribute + * @see SessionAttribute * @see SessionAttributes * @see InitBinder * @see org.springframework.web.context.request.WebRequest diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java index 8ad9c1e9..8843eb12 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/ResponseStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,10 @@ import org.springframework.http.HttpStatus; * preferable to use a {@link org.springframework.http.ResponseEntity} as * a return type and avoid the use of {@code @ResponseStatus} altogether. * + * <p>Note that a controller class may also be annotated with + * {@code @ResponseStatus} and is then inherited by all {@code @RequestMapping} + * methods. + * * @author Arjen Poutsma * @author Sam Brannen * @see org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java index cdd3772b..2287f652 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,13 +25,21 @@ import java.lang.annotation.Target; import org.springframework.stereotype.Controller; /** - * A convenience annotation that is itself annotated with {@link Controller @Controller} - * and {@link ResponseBody @ResponseBody}. + * A convenience annotation that is itself annotated with + * {@link Controller @Controller} and {@link ResponseBody @ResponseBody}. * <p> * Types that carry this annotation are treated as controllers where * {@link RequestMapping @RequestMapping} methods assume * {@link ResponseBody @ResponseBody} semantics by default. * + * <p><b>NOTE:</b> {@code @RestController} is processed if an appropriate + * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the + * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter} + * pair which are the default in the MVC Java config and the MVC namespace. + * In particular {@code @RestController} is not supported with the + * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter} + * pair both of which are also deprecated. + * * @author Rossen Stoyanchev * @author Sam Brannen * @since 4.0 diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java new file mode 100644 index 00000000..29b08076 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * A convenience annotation that is itself annotated with + * {@link ControllerAdvice @ControllerAdvice} + * and {@link ResponseBody @ResponseBody}. + * + * <p>Types that carry this annotation are treated as controller advice where + * {@link ExceptionHandler @ExceptionHandler} methods assume + * {@link ResponseBody @ResponseBody} semantics by default. + * + * <p><b>NOTE:</b> {@code @RestControllerAdvice} is processed if an appropriate + * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the + * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter} pair + * which are the default in the MVC Java config and the MVC namespace. + * In particular {@code @RestControllerAdvice} is not supported with the + * {@code DefaultAnnotationHandlerMapping}-{@code AnnotationMethodHandlerAdapter} + * pair both of which are also deprecated. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ControllerAdvice +@ResponseBody +public @interface RestControllerAdvice { + + /** + * Alias for the {@link #basePackages} attribute. + * <p>Allows for more concise annotation declarations e.g.: + * {@code @ControllerAdvice("org.my.pkg")} is equivalent to + * {@code @ControllerAdvice(basePackages="org.my.pkg")}. + * @see #basePackages() + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Array of base packages. + * <p>Controllers that belong to those base packages or sub-packages thereof + * will be included, e.g.: {@code @ControllerAdvice(basePackages="org.my.pkg")} + * or {@code @ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"})}. + * <p>{@link #value} is an alias for this attribute, simply allowing for + * more concise use of the annotation. + * <p>Also consider using {@link #basePackageClasses()} as a type-safe + * alternative to String-based package names. + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #value()} for specifying the packages + * to select Controllers to be assisted by the {@code @ControllerAdvice} + * annotated class. + * <p>Consider creating a special no-op marker class or interface in each package + * that serves no purpose other than being referenced by this attribute. + */ + Class<?>[] basePackageClasses() default {}; + + /** + * Array of classes. + * <p>Controllers that are assignable to at least one of the given types + * will be assisted by the {@code @ControllerAdvice} annotated class. + */ + Class<?>[] assignableTypes() default {}; + + /** + * Array of annotations. + * <p>Controllers that are annotated with this/one of those annotation(s) + * will be assisted by the {@code @ControllerAdvice} annotated class. + * <p>Consider creating a special annotation or use a predefined one, + * like {@link RestController @RestController}. + */ + Class<? extends Annotation>[] annotations() default {}; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java new file mode 100644 index 00000000..d02e4c01 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/SessionAttribute.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to bind a method parameter to a session attribute. + * + * <p>The main motivation is to provide convenient access to existing, permanent + * session attributes (e.g. user authentication object) with an optional/required + * check and a cast to the target method parameter type. + * + * <p>For use cases that require adding or removing session attributes consider + * injecting {@code org.springframework.web.context.request.WebRequest} or + * {@code javax.servlet.http.HttpSession} into the controller method. + * + * <p>For temporary storage of model attributes in the session as part of the + * workflow for a controller, consider using {@link SessionAttributes} instead. + * + * @author Rossen Stoyanchev + * @since 4.3 + * @see RequestMapping + * @see SessionAttributes + * @see RequestAttribute + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SessionAttribute { + + /** + * Alias for {@link #name}. + */ + @AliasFor("name") + String value() default ""; + + /** + * The name of the session attribute to bind to. + * <p>The default name is inferred from the method parameter name. + */ + @AliasFor("value") + String name() default ""; + + /** + * Whether the session attribute is required. + * <p>Defaults to {@code true}, leading to an exception being thrown + * if the attribute is missing in the session or there is no session. + * Switch this to {@code false} if you prefer a {@code null} or Java 8 + * {@code java.util.Optional} if the attribute doesn't exist. + */ + boolean required() default true; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java index 98ad9992..52850cd3 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvocationException.java @@ -26,7 +26,9 @@ import org.springframework.core.NestedRuntimeException; * @author Juergen Hoeller * @since 2.5.6 * @see HandlerMethodInvoker#invokeHandlerMethod + * @deprecated as of 4.3, in favor of the {@code HandlerMethod}-based MVC infrastructure */ +@Deprecated @SuppressWarnings("serial") public class HandlerMethodInvocationException extends NestedRuntimeException { diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java index eb869333..9287882b 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -94,7 +94,9 @@ import org.springframework.web.multipart.MultipartRequest; * @author Arjen Poutsma * @since 2.5.2 * @see #invokeHandlerMethod + * @deprecated as of 4.3, in favor of the {@code HandlerMethod}-based MVC infrastructure */ +@Deprecated public class HandlerMethodInvoker { private static final String MODEL_KEY_PREFIX_STALE = SessionAttributeStore.class.getName() + ".STALE."; diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java index 9fd25c01..53d6584e 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodResolver.java @@ -48,7 +48,9 @@ import org.springframework.web.bind.annotation.SessionAttributes; * @see org.springframework.web.bind.annotation.InitBinder * @see org.springframework.web.bind.annotation.ModelAttribute * @see org.springframework.web.bind.annotation.SessionAttributes + * @deprecated as of 4.3, in favor of the {@code HandlerMethod}-based MVC infrastructure */ +@Deprecated public class HandlerMethodResolver { private final Set<Method> handlerMethods = new LinkedHashSet<Method>(); diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index 94b40ae9..ea3c0ae8 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,6 +70,9 @@ import org.springframework.web.multipart.MultipartRequest; */ public class WebRequestDataBinder extends WebDataBinder { + private static final boolean servlet3Parts = ClassUtils.hasMethod(HttpServletRequest.class, "getParts"); + + /** * Create a new WebRequestDataBinder instance, with default object name. * @param target the target object to bind onto (or {@code null} @@ -116,7 +119,7 @@ public class WebRequestDataBinder extends WebDataBinder { if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } - else if (ClassUtils.hasMethod(HttpServletRequest.class, "getParts")) { + else if (servlet3Parts) { HttpServletRequest serlvetRequest = ((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class); new Servlet3MultipartHelper(isBindEmptyMultipartFiles()).bindParts(serlvetRequest, mpvs); } diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java index eb6b5230..acbe7953 100644 --- a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.task.AsyncListenableTaskExecutor; @@ -41,15 +39,12 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.http.client.support.AsyncHttpAccessor; +import org.springframework.http.client.support.InterceptingAsyncHttpAccessor; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; -import org.springframework.util.concurrent.FailureCallback; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureAdapter; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.SuccessCallback; -import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.AbstractUriTemplateHandler; import org.springframework.web.util.UriTemplateHandler; /** @@ -74,7 +69,7 @@ import org.springframework.web.util.UriTemplateHandler; * @since 4.0 * @see RestTemplate */ -public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOperations { +public class AsyncRestTemplate extends InterceptingAsyncHttpAccessor implements AsyncRestOperations { private final RestTemplate syncTemplate; @@ -157,8 +152,28 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe } /** - * Set a custom {@link UriTemplateHandler} for expanding URI templates. - * <p>By default, RestTemplate uses {@link DefaultUriTemplateHandler}. + * Configure default URI variable values. This is a shortcut for: + * <pre class="code"> + * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler(); + * handler.setDefaultUriVariables(...); + * + * AsyncRestTemplate restTemplate = new AsyncRestTemplate(); + * restTemplate.setUriTemplateHandler(handler); + * </pre> + * @param defaultUriVariables the default URI variable values + * @since 4.3 + */ + public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) { + UriTemplateHandler handler = this.syncTemplate.getUriTemplateHandler(); + Assert.isInstanceOf(AbstractUriTemplateHandler.class, handler, + "Can only use this property in conjunction with a DefaultUriTemplateHandler"); + ((AbstractUriTemplateHandler) handler).setDefaultUriVariables(defaultUriVariables); + } + + /** + * This property has the same purpose as the corresponding property on the + * {@code RestTemplate}. For more details see + * {@link RestTemplate#setUriTemplateHandler}. * @param handler the URI template handler to use */ public void setUriTemplateHandler(UriTemplateHandler handler) { @@ -245,75 +260,37 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe // POST @Override - public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Object... uriVariables) + public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Object... uriVars) throws RestClientException { - AsyncRequestCallback requestCallback = httpEntityCallback(request); - ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor(); - ListenableFuture<HttpHeaders> headersFuture = - execute(url, HttpMethod.POST, requestCallback, headersExtractor, uriVariables); - return extractLocationHeader(headersFuture); + AsyncRequestCallback callback = httpEntityCallback(request); + ResponseExtractor<HttpHeaders> extractor = headersExtractor(); + ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor, uriVars); + return adaptToLocationHeader(future); } @Override - public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Map<String, ?> uriVariables) + public ListenableFuture<URI> postForLocation(String url, HttpEntity<?> request, Map<String, ?> uriVars) throws RestClientException { - AsyncRequestCallback requestCallback = httpEntityCallback(request); - ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor(); - ListenableFuture<HttpHeaders> headersFuture = - execute(url, HttpMethod.POST, requestCallback, headersExtractor, uriVariables); - return extractLocationHeader(headersFuture); + AsyncRequestCallback callback = httpEntityCallback(request); + ResponseExtractor<HttpHeaders> extractor = headersExtractor(); + ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor, uriVars); + return adaptToLocationHeader(future); } @Override public ListenableFuture<URI> postForLocation(URI url, HttpEntity<?> request) throws RestClientException { - AsyncRequestCallback requestCallback = httpEntityCallback(request); - ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor(); - ListenableFuture<HttpHeaders> headersFuture = - execute(url, HttpMethod.POST, requestCallback, headersExtractor); - return extractLocationHeader(headersFuture); + AsyncRequestCallback callback = httpEntityCallback(request); + ResponseExtractor<HttpHeaders> extractor = headersExtractor(); + ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor); + return adaptToLocationHeader(future); } - private static ListenableFuture<URI> extractLocationHeader(final ListenableFuture<HttpHeaders> headersFuture) { - return new ListenableFuture<URI>() { - @Override - public void addCallback(final ListenableFutureCallback<? super URI> callback) { - addCallback(callback, callback); - } - @Override - public void addCallback(final SuccessCallback<? super URI> successCallback, final FailureCallback failureCallback) { - headersFuture.addCallback(new ListenableFutureCallback<HttpHeaders>() { - @Override - public void onSuccess(HttpHeaders result) { - successCallback.onSuccess(result.getLocation()); - } - @Override - public void onFailure(Throwable ex) { - failureCallback.onFailure(ex); - } - }); - } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return headersFuture.cancel(mayInterruptIfRunning); - } - @Override - public boolean isCancelled() { - return headersFuture.isCancelled(); - } - @Override - public boolean isDone() { - return headersFuture.isDone(); - } + private static ListenableFuture<URI> adaptToLocationHeader(ListenableFuture<HttpHeaders> future) { + return new ListenableFutureAdapter<URI, HttpHeaders>(future) { @Override - public URI get() throws InterruptedException, ExecutionException { - HttpHeaders headers = headersFuture.get(); - return headers.getLocation(); - } - @Override - public URI get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - HttpHeaders headers = headersFuture.get(timeout, unit); + protected URI adapt(HttpHeaders headers) throws ExecutionException { return headers.getLocation(); } }; @@ -389,71 +366,35 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe // OPTIONS @Override - public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Object... uriVariables) throws RestClientException { - ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor(); - ListenableFuture<HttpHeaders> headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor, uriVariables); - return extractAllowHeader(headersFuture); + public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Object... uriVars) throws RestClientException { + ResponseExtractor<HttpHeaders> extractor = headersExtractor(); + ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor, uriVars); + return adaptToAllowHeader(future); } @Override - public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Map<String, ?> uriVariables) throws RestClientException { - ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor(); - ListenableFuture<HttpHeaders> headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor, uriVariables); - return extractAllowHeader(headersFuture); + public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Map<String, ?> uriVars) throws RestClientException { + ResponseExtractor<HttpHeaders> extractor = headersExtractor(); + ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor, uriVars); + return adaptToAllowHeader(future); } @Override public ListenableFuture<Set<HttpMethod>> optionsForAllow(URI url) throws RestClientException { - ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor(); - ListenableFuture<HttpHeaders> headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor); - return extractAllowHeader(headersFuture); + ResponseExtractor<HttpHeaders> extractor = headersExtractor(); + ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor); + return adaptToAllowHeader(future); } - private static ListenableFuture<Set<HttpMethod>> extractAllowHeader(final ListenableFuture<HttpHeaders> headersFuture) { - return new ListenableFuture<Set<HttpMethod>>() { + private static ListenableFuture<Set<HttpMethod>> adaptToAllowHeader(ListenableFuture<HttpHeaders> future) { + return new ListenableFutureAdapter<Set<HttpMethod>, HttpHeaders>(future) { @Override - public void addCallback(final ListenableFutureCallback<? super Set<HttpMethod>> callback) { - addCallback(callback, callback); - } - @Override - public void addCallback(final SuccessCallback<? super Set<HttpMethod>> successCallback, final FailureCallback failureCallback) { - headersFuture.addCallback(new ListenableFutureCallback<HttpHeaders>() { - @Override - public void onSuccess(HttpHeaders result) { - successCallback.onSuccess(result.getAllow()); - } - @Override - public void onFailure(Throwable ex) { - failureCallback.onFailure(ex); - } - }); - } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return headersFuture.cancel(mayInterruptIfRunning); - } - @Override - public boolean isCancelled() { - return headersFuture.isCancelled(); - } - @Override - public boolean isDone() { - return headersFuture.isDone(); - } - @Override - public Set<HttpMethod> get() throws InterruptedException, ExecutionException { - HttpHeaders headers = headersFuture.get(); - return headers.getAllow(); - } - @Override - public Set<HttpMethod> get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - HttpHeaders headers = headersFuture.get(timeout, unit); + protected Set<HttpMethod> adapt(HttpHeaders headers) throws ExecutionException { return headers.getAllow(); } }; } - // exchange @Override diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java index 45eb677e..591b95e6 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java @@ -113,7 +113,7 @@ public class DefaultResponseErrorHandler implements ResponseErrorHandler { private Charset getCharset(ClientHttpResponse response) { HttpHeaders headers = response.getHeaders(); MediaType contentType = headers.getContentType(); - return contentType != null ? contentType.getCharSet() : null; + return contentType != null ? contentType.getCharset() : null; } } diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java index 7eada90c..2f03908c 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java @@ -76,7 +76,7 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> { @Override - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"unchecked", "rawtypes", "resource"}) public T extractData(ClientHttpResponse response) throws IOException { MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response); if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) { diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java b/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java index 93b3c110..0d5c2fbc 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.web.client; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import org.springframework.http.HttpHeaders; @@ -27,29 +26,19 @@ import org.springframework.http.HttpStatus; * * @author Arjen Poutsma * @author Chris Beams + * @author Rossen Stoyanchev * @since 3.0 */ -public abstract class HttpStatusCodeException extends RestClientException { +public abstract class HttpStatusCodeException extends RestClientResponseException { - private static final long serialVersionUID = -5807494703720513267L; - - private static final String DEFAULT_CHARSET = "ISO-8859-1"; + private static final long serialVersionUID = 5696801857651587810L; private final HttpStatus statusCode; - private final String statusText; - - private final byte[] responseBody; - - private final HttpHeaders responseHeaders; - - private final String responseCharset; - /** - * Construct a new instance of {@code HttpStatusCodeException} based on an - * {@link HttpStatus}. + * Construct a new instance with an {@link HttpStatus}. * @param statusCode the status code */ protected HttpStatusCodeException(HttpStatus statusCode) { @@ -57,8 +46,7 @@ public abstract class HttpStatusCodeException extends RestClientException { } /** - * Construct a new instance of {@code HttpStatusCodeException} based on an - * {@link HttpStatus} and status text. + * Construct a new instance with an {@link HttpStatus} and status text. * @param statusCode the status code * @param statusText the status text */ @@ -67,23 +55,22 @@ public abstract class HttpStatusCodeException extends RestClientException { } /** - * Construct a new instance of {@code HttpStatusCodeException} based on an - * {@link HttpStatus}, status text, and response body content. + * Construct instance with an {@link HttpStatus}, status text, and content. * @param statusCode the status code * @param statusText the status text * @param responseBody the response body content, may be {@code null} * @param responseCharset the response body charset, may be {@code null} * @since 3.0.5 */ - protected HttpStatusCodeException( - HttpStatus statusCode, String statusText, byte[] responseBody, Charset responseCharset) { + protected HttpStatusCodeException(HttpStatus statusCode, String statusText, + byte[] responseBody, Charset responseCharset) { this(statusCode, statusText, null, responseBody, responseCharset); } /** - * Construct a new instance of {@code HttpStatusCodeException} based on an - * {@link HttpStatus}, status text, and response body content. + * Construct instance with an {@link HttpStatus}, status text, content, and + * a response charset. * @param statusCode the status code * @param statusText the status text * @param responseHeaders the response headers, may be {@code null} @@ -94,12 +81,9 @@ public abstract class HttpStatusCodeException extends RestClientException { protected HttpStatusCodeException(HttpStatus statusCode, String statusText, HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset) { - super(statusCode.value() + " " + statusText); + super(statusCode.value() + " " + statusText, statusCode.value(), statusText, + responseHeaders, responseBody, responseCharset); this.statusCode = statusCode; - this.statusText = statusText; - this.responseHeaders = responseHeaders; - this.responseBody = responseBody != null ? responseBody : new byte[0]; - this.responseCharset = responseCharset != null ? responseCharset.name() : DEFAULT_CHARSET; } @@ -110,41 +94,4 @@ public abstract class HttpStatusCodeException extends RestClientException { return this.statusCode; } - /** - * Return the HTTP status text. - */ - public String getStatusText() { - return this.statusText; - } - - /** - * Return the HTTP response headers. - * @since 3.1.2 - */ - public HttpHeaders getResponseHeaders() { - return this.responseHeaders; - } - - /** - * Return the response body as a byte array. - * @since 3.0.5 - */ - public byte[] getResponseBodyAsByteArray() { - return this.responseBody; - } - - /** - * Return the response body as a string. - * @since 3.0.5 - */ - public String getResponseBodyAsString() { - try { - return new String(this.responseBody, this.responseCharset); - } - catch (UnsupportedEncodingException ex) { - // should not occur - throw new IllegalStateException(ex); - } - } - } diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java new file mode 100644 index 00000000..d9c1e6a0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/RestClientResponseException.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.client; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +import org.springframework.http.HttpHeaders; + +/** + * Common base class for exceptions that contain actual HTTP response data. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class RestClientResponseException extends RestClientException { + + private static final long serialVersionUID = -8803556342728481792L; + + private static final String DEFAULT_CHARSET = "ISO-8859-1"; + + + private final int rawStatusCode; + + private final String statusText; + + private final byte[] responseBody; + + private final HttpHeaders responseHeaders; + + private final String responseCharset; + + + /** + * Construct a new instance of with the given response data. + * @param statusCode the raw status code value + * @param statusText the status text + * @param responseHeaders the response headers (may be {@code null}) + * @param responseBody the response body content (may be {@code null}) + * @param responseCharset the response body charset (may be {@code null}) + */ + public RestClientResponseException(String message, int statusCode, String statusText, + HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset) { + + super(message); + this.rawStatusCode = statusCode; + this.statusText = statusText; + this.responseHeaders = responseHeaders; + this.responseBody = (responseBody != null ? responseBody : new byte[0]); + this.responseCharset = (responseCharset != null ? responseCharset.name() : DEFAULT_CHARSET); + } + + + /** + * Return the raw HTTP status code value. + */ + public int getRawStatusCode() { + return this.rawStatusCode; + } + + /** + * Return the HTTP status text. + */ + public String getStatusText() { + return this.statusText; + } + + /** + * Return the HTTP response headers. + */ + public HttpHeaders getResponseHeaders() { + return this.responseHeaders; + } + + /** + * Return the response body as a byte array. + */ + public byte[] getResponseBodyAsByteArray() { + return this.responseBody; + } + + /** + * Return the response body as a string. + */ + public String getResponseBodyAsString() { + try { + return new String(this.responseBody, this.responseCharset); + } + catch (UnsupportedEncodingException ex) { + // should not occur + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 8c7c0433..8a7f79a2 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -51,6 +51,7 @@ import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConve import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.web.util.AbstractUriTemplateHandler; import org.springframework.web.util.DefaultUriTemplateHandler; import org.springframework.web.util.UriTemplateHandler; @@ -237,8 +238,30 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } /** - * Set a custom {@link UriTemplateHandler} for expanding URI templates. - * <p>By default, RestTemplate uses {@link DefaultUriTemplateHandler}. + * Configure default URI variable values. This is a shortcut for: + * <pre class="code"> + * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler(); + * handler.setDefaultUriVariables(...); + * + * RestTemplate restTemplate = new RestTemplate(); + * restTemplate.setUriTemplateHandler(handler); + * </pre> + * @param defaultUriVariables the default URI variable values + * @since 4.3 + */ + public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) { + Assert.isInstanceOf(AbstractUriTemplateHandler.class, this.uriTemplateHandler, + "Can only use this property in conjunction with an AbstractUriTemplateHandler"); + ((AbstractUriTemplateHandler) this.uriTemplateHandler).setDefaultUriVariables(defaultUriVariables); + } + + /** + * Configure the {@link UriTemplateHandler} to use to expand URI templates. + * By default the {@link DefaultUriTemplateHandler} is used which relies on + * Spring's URI template support and exposes several useful properties that + * customize its behavior for encoding and for prepending a common base URL. + * An alternative implementation may be used to plug an external URI + * template library. * @param handler the URI template handler to use */ public void setUriTemplateHandler(UriTemplateHandler handler) { @@ -603,8 +626,11 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } } catch (IOException ex) { + String resource = url.toString(); + String query = url.getRawQuery(); + resource = (query != null ? resource.substring(0, resource.indexOf(query) - 1) : resource); throw new ResourceAccessException("I/O error on " + method.name() + - " request for \"" + url + "\": " + ex.getMessage(), ex); + " request for \"" + resource + "\": " + ex.getMessage(), ex); } finally { if (response != null) { @@ -728,7 +754,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat List<MediaType> supportedMediaTypes = messageConverter.getSupportedMediaTypes(); List<MediaType> result = new ArrayList<MediaType>(supportedMediaTypes.size()); for (MediaType supportedMediaType : supportedMediaTypes) { - if (supportedMediaType.getCharSet() != null) { + if (supportedMediaType.getCharset() != null) { supportedMediaType = new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); } @@ -779,11 +805,34 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } else { Object requestBody = this.requestEntity.getBody(); - Class<?> requestType = requestBody.getClass(); + Class<?> requestBodyClass = requestBody.getClass(); + Type requestBodyType = (this.requestEntity instanceof RequestEntity ? + ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass); HttpHeaders requestHeaders = this.requestEntity.getHeaders(); MediaType requestContentType = requestHeaders.getContentType(); for (HttpMessageConverter<?> messageConverter : getMessageConverters()) { - if (messageConverter.canWrite(requestType, requestContentType)) { + if (messageConverter instanceof GenericHttpMessageConverter) { + GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter; + if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) { + if (!requestHeaders.isEmpty()) { + httpRequest.getHeaders().putAll(requestHeaders); + } + if (logger.isDebugEnabled()) { + if (requestContentType != null) { + logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + + "\" using [" + messageConverter + "]"); + } + else { + logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]"); + } + + } + genericMessageConverter.write( + requestBody, requestBodyType, requestContentType, httpRequest); + return; + } + } + else if (messageConverter.canWrite(requestBodyClass, requestContentType)) { if (!requestHeaders.isEmpty()) { httpRequest.getHeaders().putAll(requestHeaders); } @@ -803,7 +852,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } } String message = "Could not write request: no suitable HttpMessageConverter found for request type [" + - requestType.getName() + "]"; + requestBodyClass.getName() + "]"; if (requestContentType != null) { message += " and content type [" + requestContentType + "]"; } diff --git a/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java b/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java index 40ac5047..b8e894b9 100644 --- a/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java +++ b/spring-web/src/main/java/org/springframework/web/client/UnknownHttpStatusCodeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.web.client; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import org.springframework.http.HttpHeaders; @@ -28,21 +27,9 @@ import org.springframework.http.HttpStatus; * @author Rossen Stoyanchev * @since 3.2 */ -public class UnknownHttpStatusCodeException extends RestClientException { +public class UnknownHttpStatusCodeException extends RestClientResponseException { - private static final long serialVersionUID = 4702443689088991600L; - - private static final String DEFAULT_CHARSET = "ISO-8859-1"; - - private final int rawStatusCode; - - private final String statusText; - - private final byte[] responseBody; - - private final HttpHeaders responseHeaders; - - private final String responseCharset; + private static final long serialVersionUID = 7103980251635005491L; /** @@ -57,54 +44,8 @@ public class UnknownHttpStatusCodeException extends RestClientException { public UnknownHttpStatusCodeException(int rawStatusCode, String statusText, HttpHeaders responseHeaders, byte[] responseBody, Charset responseCharset) { - super("Unknown status code [" + String.valueOf(rawStatusCode) + "]" + " " + statusText); - this.rawStatusCode = rawStatusCode; - this.statusText = statusText; - this.responseHeaders = responseHeaders; - this.responseBody = responseBody != null ? responseBody : new byte[0]; - this.responseCharset = responseCharset != null ? responseCharset.name() : DEFAULT_CHARSET; - } - - - /** - * Return the raw HTTP status code value. - */ - public int getRawStatusCode() { - return this.rawStatusCode; - } - - /** - * Return the HTTP status text. - */ - public String getStatusText() { - return this.statusText; - } - - /** - * Return the HTTP response headers. - */ - public HttpHeaders getResponseHeaders() { - return this.responseHeaders; - } - - /** - * Return the response body as a byte array. - */ - public byte[] getResponseBodyAsByteArray() { - return this.responseBody; - } - - /** - * Return the response body as a string. - */ - public String getResponseBodyAsString() { - try { - return new String(this.responseBody, this.responseCharset); - } - catch (UnsupportedEncodingException ex) { - // should not occur - throw new IllegalStateException(ex); - } + super("Unknown status code [" + String.valueOf(rawStatusCode) + "]" + " " + statusText, + rawStatusCode, statusText, responseHeaders, responseBody, responseCharset); } } diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java b/spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java new file mode 100644 index 00000000..fa7e897c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/annotation/ApplicationScope.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.context.WebApplicationContext; + +/** + * {@code @ApplicationScope} is a specialization of {@link Scope @Scope} for a + * component whose lifecycle is bound to the current web application. + * + * <p>Specifically, {@code @ApplicationScope} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @Scope("application")} with the default + * {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}. + * + * <p>{@code @ApplicationScope} may be used as a meta-annotation to create custom + * composed annotations. + * + * @author Sam Brannen + * @since 4.3 + * @see RequestScope + * @see SessionScope + * @see org.springframework.context.annotation.Scope + * @see org.springframework.web.context.WebApplicationContext#SCOPE_APPLICATION + * @see org.springframework.web.context.support.ServletContextScope + * @see org.springframework.stereotype.Component + * @see org.springframework.context.annotation.Bean + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Scope(WebApplicationContext.SCOPE_APPLICATION) +public @interface ApplicationScope { + + /** + * Alias for {@link Scope#proxyMode}. + * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}. + */ + @AliasFor(annotation = Scope.class) + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java b/spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java new file mode 100644 index 00000000..f33a1dd7 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/annotation/RequestScope.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.context.WebApplicationContext; + +/** + * {@code @RequestScope} is a specialization of {@link Scope @Scope} for a + * component whose lifecycle is bound to the current web request. + * + * <p>Specifically, {@code @RequestScope} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @Scope("request")} with the default + * {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}. + * + * <p>{@code @RequestScope} may be used as a meta-annotation to create custom + * composed annotations. + * + * @author Sam Brannen + * @since 4.3 + * @see SessionScope + * @see ApplicationScope + * @see org.springframework.context.annotation.Scope + * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST + * @see org.springframework.web.context.request.RequestScope + * @see org.springframework.stereotype.Component + * @see org.springframework.context.annotation.Bean + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Scope(WebApplicationContext.SCOPE_REQUEST) +public @interface RequestScope { + + /** + * Alias for {@link Scope#proxyMode}. + * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}. + */ + @AliasFor(annotation = Scope.class) + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java b/spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java new file mode 100644 index 00000000..45af3c6e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/annotation/SessionScope.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.context.WebApplicationContext; + +/** + * {@code @SessionScope} is a specialization of {@link Scope @Scope} for a + * component whose lifecycle is bound to the current web session. + * + * <p>Specifically, {@code @SessionScope} is a <em>composed annotation</em> that + * acts as a shortcut for {@code @Scope("session")} with the default + * {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}. + * + * <p>{@code @SessionScope} may be used as a meta-annotation to create custom + * composed annotations. + * + * @author Sam Brannen + * @since 4.3 + * @see RequestScope + * @see ApplicationScope + * @see org.springframework.context.annotation.Scope + * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION + * @see org.springframework.web.context.request.SessionScope + * @see org.springframework.stereotype.Component + * @see org.springframework.context.annotation.Bean + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Scope(WebApplicationContext.SCOPE_SESSION) +public @interface SessionScope { + + /** + * Alias for {@link Scope#proxyMode}. + * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}. + */ + @AliasFor(annotation = Scope.class) + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java b/spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java new file mode 100644 index 00000000..fffce1b0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/annotation/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides convenience annotations for web scopes. + */ +package org.springframework.web.context.annotation; diff --git a/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java b/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java index 73301c34..bfbf032b 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/FacesRequestAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ public class FacesRequestAttributes implements RequestAttributes { Object session = getExternalContext().getSession(true); try { // Both HttpSession and PortletSession have a getId() method. - Method getIdMethod = session.getClass().getMethod("getId", new Class<?>[0]); + Method getIdMethod = session.getClass().getMethod("getId"); return ReflectionUtils.invokeMethod(getIdMethod, session).toString(); } catch (NoSuchMethodException ex) { @@ -227,12 +227,12 @@ public class FacesRequestAttributes implements RequestAttributes { @Override public Object getSessionMutex() { - // Enforce presence of a session first to allow listeners - // to create the mutex attribute, if any. - Object session = getExternalContext().getSession(true); - Object mutex = getExternalContext().getSessionMap().get(WebUtils.SESSION_MUTEX_ATTRIBUTE); + // Enforce presence of a session first to allow listeners to create the mutex attribute + ExternalContext externalContext = getExternalContext(); + Object session = externalContext.getSession(true); + Object mutex = externalContext.getSessionMap().get(WebUtils.SESSION_MUTEX_ATTRIBUTE); if (mutex == null) { - mutex = session; + mutex = (session != null ? session : externalContext); } return mutex; } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java index 4182342a..6eb5de18 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletRequestAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,15 +108,24 @@ public class ServletRequestAttributes extends AbstractRequestAttributes { */ protected final HttpSession getSession(boolean allowCreate) { if (isRequestActive()) { - return this.request.getSession(allowCreate); + HttpSession session = this.request.getSession(allowCreate); + this.session = session; + return session; } else { // Access through stored session reference, if any... - if (this.session == null && allowCreate) { - throw new IllegalStateException( - "No session found and request already completed - cannot create new session!"); + HttpSession session = this.session; + if (session == null) { + if (allowCreate) { + throw new IllegalStateException( + "No session found and request already completed - cannot create new session!"); + } + else { + session = this.request.getSession(false); + this.session = session; + } } - return this.session; + return session; } } @@ -251,25 +260,26 @@ public class ServletRequestAttributes extends AbstractRequestAttributes { */ @Override protected void updateAccessedSessionAttributes() { - // Store session reference for access after request completion. - this.session = this.request.getSession(false); - // Update all affected session attributes. - if (this.session != null) { - try { - for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) { - String name = entry.getKey(); - Object newValue = entry.getValue(); - Object oldValue = this.session.getAttribute(name); - if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) { - this.session.setAttribute(name, newValue); + if (!this.sessionAttributesToUpdate.isEmpty()) { + // Update all affected session attributes. + HttpSession session = getSession(false); + if (session != null) { + try { + for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) { + String name = entry.getKey(); + Object newValue = entry.getValue(); + Object oldValue = session.getAttribute(name); + if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) { + session.setAttribute(name, newValue); + } } } + catch (IllegalStateException ex) { + // Session invalidated - shouldn't usually happen. + } } - catch (IllegalStateException ex) { - // Session invalidated - shouldn't usually happen. - } + this.sessionAttributesToUpdate.clear(); } - this.sessionAttributesToUpdate.clear(); } /** diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index 6cc8e052..97a227af 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import java.util.Date; import java.util.Iterator; import java.util.Locale; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -47,6 +49,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; private static final String HEADER_LAST_MODIFIED = "Last-Modified"; @@ -55,6 +59,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final String METHOD_HEAD = "HEAD"; + private static final String METHOD_POST = "POST"; + + private static final String METHOD_PUT = "PUT"; + + private static final String METHOD_DELETE = "DELETE"; + + /** + * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match" + * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> + */ + private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); + /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */ private static final boolean servlet3Present = @@ -183,11 +199,18 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ if (isCompatibleWithConditionalRequests(response)) { this.notModified = isTimestampNotModified(lastModifiedTimestamp); if (response != null) { - if (this.notModified && supportsNotModifiedStatus()) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + if (supportsNotModifiedStatus()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { + response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + } } - if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { - response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + else if (supportsConditionalUpdate()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + } } } } @@ -201,7 +224,9 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ if (StringUtils.hasLength(etag) && !this.notModified) { if (isCompatibleWithConditionalRequests(response)) { etag = addEtagPadding(etag); - this.notModified = isEtagNotModified(etag); + if (hasRequestHeader(HEADER_IF_NONE_MATCH)) { + this.notModified = isEtagNotModified(etag); + } if (response != null) { if (this.notModified && supportsNotModifiedStatus()) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); @@ -221,16 +246,28 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ if (StringUtils.hasLength(etag) && !this.notModified) { if (isCompatibleWithConditionalRequests(response)) { etag = addEtagPadding(etag); - this.notModified = isEtagNotModified(etag) && isTimestampNotModified(lastModifiedTimestamp); + if (hasRequestHeader(HEADER_IF_NONE_MATCH)) { + this.notModified = isEtagNotModified(etag); + } + else if (hasRequestHeader(HEADER_IF_MODIFIED_SINCE)) { + this.notModified = isTimestampNotModified(lastModifiedTimestamp); + } if (response != null) { - if (this.notModified && supportsNotModifiedStatus()) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } - if (isHeaderAbsent(response, HEADER_ETAG)) { - response.setHeader(HEADER_ETAG, etag); + if (supportsNotModifiedStatus()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + if (isHeaderAbsent(response, HEADER_ETAG)) { + response.setHeader(HEADER_ETAG, etag); + } + if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { + response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + } } - if (isHeaderAbsent(response, HEADER_LAST_MODIFIED)) { - response.setDateHeader(HEADER_LAST_MODIFIED, lastModifiedTimestamp); + else if (supportsConditionalUpdate()) { + if (this.notModified) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + } } } } @@ -250,7 +287,8 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return true; } return HttpStatus.valueOf(response.getStatus()).is2xxSuccessful(); - } catch (IllegalArgumentException e) { + } + catch (IllegalArgumentException e) { return true; } } @@ -263,47 +301,65 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return (response.getHeader(header) == null); } + private boolean hasRequestHeader(String headerName) { + return StringUtils.hasLength(getHeader(headerName)); + } + private boolean supportsNotModifiedStatus() { String method = getRequest().getMethod(); return (METHOD_GET.equals(method) || METHOD_HEAD.equals(method)); } - @SuppressWarnings("deprecation") + private boolean supportsConditionalUpdate() { + String method = getRequest().getMethod(); + return (METHOD_POST.equals(method) || METHOD_PUT.equals(method) || METHOD_DELETE.equals(method)) + && hasRequestHeader(HEADER_IF_UNMODIFIED_SINCE); + } + private boolean isTimestampNotModified(long lastModifiedTimestamp) { - long ifModifiedSince = -1; + long ifModifiedSince = parseDateHeader(HEADER_IF_MODIFIED_SINCE); + if (ifModifiedSince != -1) { + return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); + } + long ifUnmodifiedSince = parseDateHeader(HEADER_IF_UNMODIFIED_SINCE); + if (ifUnmodifiedSince != -1) { + return (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000)); + } + return false; + } + + @SuppressWarnings("deprecation") + private long parseDateHeader(String headerName) { + long dateValue = -1; try { - ifModifiedSince = getRequest().getDateHeader(HEADER_IF_MODIFIED_SINCE); + dateValue = getRequest().getDateHeader(headerName); } catch (IllegalArgumentException ex) { - String headerValue = getRequest().getHeader(HEADER_IF_MODIFIED_SINCE); + String headerValue = getHeader(headerName); // Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774" int separatorIndex = headerValue.indexOf(';'); if (separatorIndex != -1) { String datePart = headerValue.substring(0, separatorIndex); try { - ifModifiedSince = Date.parse(datePart); + dateValue = Date.parse(datePart); } catch (IllegalArgumentException ex2) { // Giving up } } } - return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); + return dateValue; } private boolean isEtagNotModified(String etag) { - if (StringUtils.hasLength(etag)) { - String ifNoneMatch = getRequest().getHeader(HEADER_IF_NONE_MATCH); - if (StringUtils.hasLength(ifNoneMatch)) { - String[] clientEtags = StringUtils.delimitedListToStringArray(ifNoneMatch, ",", " "); - for (String clientEtag : clientEtags) { - // compare weak/strong ETag as per https://tools.ietf.org/html/rfc7232#section-2.3 - if (StringUtils.hasLength(clientEtag) && - (clientEtag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", "")) || - clientEtag.equals("*"))) { - return true; - } - } + String ifNoneMatch = getHeader(HEADER_IF_NONE_MATCH); + // compare weak/strong ETag as per https://tools.ietf.org/html/rfc7232#section-2.3 + String serverETag = etag.replaceFirst("^W/", ""); + Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(ifNoneMatch); + while (eTagMatcher.find()) { + if ("*".equals(eTagMatcher.group()) + || serverETag.equals(eTagMatcher.group(3))) { + return true; } } return false; @@ -316,7 +372,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ return etag; } - @Override public String getDescription(boolean includeClientInfo) { HttpServletRequest request = getRequest(); diff --git a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java index cea9fa70..c4c11eb4 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/WebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,10 +126,10 @@ public interface WebRequest extends RequestAttributes { boolean isSecure(); /** - * Check whether the request qualifies as not modified given the + * Check whether the requested resource has been modified given the * supplied last-modified timestamp (as determined by the application). - * <p>This will also transparently set the appropriate response headers, - * for both the modified case and the not-modified case. + * <p>This will also transparently set the "Last-Modified" response header + * and HTTP status when applicable. * <p>Typical usage: * <pre class="code"> * public String myHandleMethod(WebRequest webRequest, Model model) { @@ -142,6 +142,8 @@ public interface WebRequest extends RequestAttributes { * model.addAttribute(...); * return "myViewName"; * }</pre> + * <p>This method works with conditional GET/HEAD requests, but + * also with conditional POST/PUT/DELETE requests. * <p><strong>Note:</strong> you can use either * this {@code #checkNotModified(long)} method; or * {@link #checkNotModified(String)}. If you want enforce both @@ -151,8 +153,9 @@ public interface WebRequest extends RequestAttributes { * <p>If the "If-Modified-Since" header is set but cannot be parsed * to a date value, this method will ignore the header and proceed * with setting the last-modified timestamp on the response. - * @param lastModifiedTimestamp the last-modified timestamp that - * the application determined for the underlying resource + * @param lastModifiedTimestamp the last-modified timestamp in + * milliseconds that the application determined for the underlying + * resource * @return whether the request qualifies as not modified, * allowing to abort request processing and relying on the response * telling the client that the content has not been modified @@ -160,10 +163,10 @@ public interface WebRequest extends RequestAttributes { boolean checkNotModified(long lastModifiedTimestamp); /** - * Check whether the request qualifies as not modified given the + * Check whether the requested resource has been modified given the * supplied {@code ETag} (entity tag), as determined by the application. - * <p>This will also transparently set the appropriate response headers, - * for both the modified case and the not-modified case. + * <p>This will also transparently set the "ETag" response header + * and HTTP status when applicable. * <p>Typical usage: * <pre class="code"> * public String myHandleMethod(WebRequest webRequest, Model model) { @@ -185,18 +188,16 @@ public interface WebRequest extends RequestAttributes { * @param etag the entity tag that the application determined * for the underlying resource. This parameter will be padded * with quotes (") if necessary. - * @return whether the request qualifies as not modified, - * allowing to abort request processing and relying on the response - * telling the client that the content has not been modified + * @return true if the request does not require further processing. */ boolean checkNotModified(String etag); /** - * Check whether the request qualifies as not modified given the + * Check whether the requested resource has been modified given the * supplied {@code ETag} (entity tag) and last-modified timestamp, * as determined by the application. * <p>This will also transparently set the "ETag" and "Last-Modified" - * response headers, for both the modified case and the not-modified case. + * response headers, and HTTP status when applicable. * <p>Typical usage: * <pre class="code"> * public String myHandleMethod(WebRequest webRequest, Model model) { @@ -210,6 +211,8 @@ public interface WebRequest extends RequestAttributes { * model.addAttribute(...); * return "myViewName"; * }</pre> + * <p>This method works with conditional GET/HEAD requests, but + * also with conditional POST/PUT/DELETE requests. * <p><strong>Note:</strong> The HTTP specification recommends * setting both ETag and Last-Modified values, but you can also * use {@code #checkNotModified(String)} or @@ -217,11 +220,10 @@ public interface WebRequest extends RequestAttributes { * @param etag the entity tag that the application determined * for the underlying resource. This parameter will be padded * with quotes (") if necessary. - * @param lastModifiedTimestamp the last-modified timestamp that - * the application determined for the underlying resource - * @return whether the request qualifies as not modified, - * allowing to abort request processing and relying on the response - * telling the client that the content has not been modified + * @param lastModifiedTimestamp the last-modified timestamp in + * milliseconds that the application determined for the underlying + * resource + * @return true if the request does not require further processing. * @since 4.2 */ boolean checkNotModified(String etag, long lastModifiedTimestamp); diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java index 638068a3..89e5745f 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/DeferredResultProcessingInterceptor.java @@ -25,7 +25,7 @@ import org.springframework.web.context.request.NativeWebRequest; * * <p>A {@code DeferredResultProcessingInterceptor} is invoked before the start * of async processing, after the {@code DeferredResult} is set as well as on - * timeout, or or after completing for any reason including a timeout or network + * timeout, or after completing for any reason including a timeout or network * error. * * <p>As a general rule exceptions raised by interceptor methods will cause diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index fc0bb983..9bd4ac5c 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -34,7 +34,7 @@ import org.springframework.web.context.request.ServletWebRequest; * * <p>The servlet and all filters involved in an async request must have async * support enabled using the Servlet API or by adding an - * {@code <async-support>true</async-support>} element to servlet and filter + * {@code <async-supported>true</async-supported>} element to servlet and filter * declarations in {@code web.xml}. * * @author Rossen Stoyanchev diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index a20cf23d..83913f02 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,7 +105,6 @@ public final class WebAsyncManager { */ public void setAsyncWebRequest(final AsyncWebRequest asyncWebRequest) { Assert.notNull(asyncWebRequest, "AsyncWebRequest must not be null"); - Assert.state(!isConcurrentHandlingStarted(), "Can't set AsyncWebRequest with concurrent handling in progress"); this.asyncWebRequest = asyncWebRequest; this.asyncWebRequest.addCompletionHandler(new Runnable() { @Override diff --git a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java index a05b782f..a13890b0 100644 --- a/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java +++ b/spring-web/src/main/java/org/springframework/web/context/support/ServletContextResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,7 @@ public class ServletContextResource extends AbstractFileResolvingResource implem this.path = pathToUse; } + /** * Return the ServletContext for this resource. */ @@ -91,7 +92,6 @@ public class ServletContextResource extends AbstractFileResolvingResource implem return this.path; } - /** * This implementation checks {@code ServletContext.getResource}. * @see javax.servlet.ServletContext#getResource(String) diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 76daa33b..016a355b 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -340,6 +340,7 @@ public class CorsConfiguration { } if (allowedMethods.isEmpty()) { allowedMethods.add(HttpMethod.GET.name()); + allowedMethods.add(HttpMethod.HEAD.name()); } List<HttpMethod> result = new ArrayList<HttpMethod>(allowedMethods.size()); boolean allowed = false; diff --git a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java index 4062bf6d..6a9af746 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/AbstractRequestLoggingFilter.java @@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.util.ContentCachingRequestWrapper; @@ -72,6 +73,8 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter private boolean includeClientInfo = false; + private boolean includeHeaders = false; + private boolean includePayload = false; private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH; @@ -120,6 +123,24 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter } /** + * Set whether the request headers should be included in the log message. + * <p>Should be configured using an {@code <init-param>} for parameter name + * "includeHeaders" in the filter definition in {@code web.xml}. + * @since 4.3 + */ + public void setIncludeHeaders(boolean includeHeaders) { + this.includeHeaders = includeHeaders; + } + + /** + * Return whether the request headers should be included in the log message. + * @since 4.3 + */ + public boolean isIncludeHeaders() { + return this.includeHeaders; + } + + /** * Set whether the request payload (body) should be included in the log message. * <p>Should be configured using an {@code <init-param>} for parameter name * "includePayload" in the filter definition in {@code web.xml}. @@ -276,6 +297,10 @@ public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter } } + if (isIncludeHeaders()) { + msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders()); + } + if (isIncludePayload()) { ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); diff --git a/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java b/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java index 56a7c899..fc564f3f 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/CharacterEncodingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,9 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { private String encoding; - private boolean forceEncoding = false; + private boolean forceRequestEncoding = false; + + private boolean forceResponseEncoding = false; /** @@ -77,9 +79,26 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { * @see #setForceEncoding */ public CharacterEncodingFilter(String encoding, boolean forceEncoding) { + this(encoding, forceEncoding, forceEncoding); + } + + /** + * Create a {@code CharacterEncodingFilter} for the given encoding. + * @param encoding the encoding to apply + * @param forceRequestEncoding whether the specified encoding is supposed to + * override existing request encodings + * @param forceResponseEncoding whether the specified encoding is supposed to + * override existing response encodings + * @since 4.3 + * @see #setEncoding + * @see #setForceRequestEncoding(boolean) + * @see #setForceResponseEncoding(boolean) + */ + public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) { Assert.hasLength(encoding, "Encoding must not be empty"); this.encoding = encoding; - this.forceEncoding = forceEncoding; + this.forceRequestEncoding = forceRequestEncoding; + this.forceResponseEncoding = forceResponseEncoding; } @@ -95,15 +114,69 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { } /** + * Return the configured encoding for requests and/or responses + * @since 4.3 + */ + public String getEncoding() { + return this.encoding; + } + + /** * Set whether the configured {@link #setEncoding encoding} of this filter * is supposed to override existing request and response encodings. * <p>Default is "false", i.e. do not modify the encoding if * {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()} * returns a non-null value. Switch this to "true" to enforce the specified * encoding in any case, applying it as default response encoding as well. + * <p>This is the equivalent to setting both {@link #setForceRequestEncoding(boolean)} + * and {@link #setForceResponseEncoding(boolean)}. + * @see #setForceRequestEncoding(boolean) + * @see #setForceResponseEncoding(boolean) */ public void setForceEncoding(boolean forceEncoding) { - this.forceEncoding = forceEncoding; + this.forceRequestEncoding = forceEncoding; + this.forceResponseEncoding = forceEncoding; + } + + /** + * Set whether the configured {@link #setEncoding encoding} of this filter + * is supposed to override existing request encodings. + * <p>Default is "false", i.e. do not modify the encoding if + * {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()} + * returns a non-null value. Switch this to "true" to enforce the specified + * encoding in any case. + * @since 4.3 + */ + public void setForceRequestEncoding(boolean forceRequestEncoding) { + this.forceRequestEncoding = forceRequestEncoding; + } + + /** + * Return whether the encoding should be forced on requests + * @since 4.3 + */ + public boolean isForceRequestEncoding() { + return this.forceRequestEncoding; + } + + /** + * Set whether the configured {@link #setEncoding encoding} of this filter + * is supposed to override existing response encodings. + * <p>Default is "false", i.e. do not modify the encoding. + * Switch this to "true" to enforce the specified encoding + * for responses in any case. + * @since 4.3 + */ + public void setForceResponseEncoding(boolean forceResponseEncoding) { + this.forceResponseEncoding = forceResponseEncoding; + } + + /** + * Return whether the encoding should be forced on responses. + * @since 4.3 + */ + public boolean isForceResponseEncoding() { + return this.forceResponseEncoding; } @@ -112,10 +185,13 @@ public class CharacterEncodingFilter extends OncePerRequestFilter { HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) { - request.setCharacterEncoding(this.encoding); - if (this.forceEncoding) { - response.setCharacterEncoding(this.encoding); + String encoding = getEncoding(); + if (encoding != null) { + if (isForceRequestEncoding() || request.getCharacterEncoding() == null) { + request.setCharacterEncoding(encoding); + } + if (isForceResponseEncoding()) { + response.setCharacterEncoding(encoding); } } filterChain.doFilter(request, response); diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java new file mode 100644 index 00000000..a1196ec2 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -0,0 +1,225 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.filter; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpRequest; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UrlPathHelper; + +/** + * Filter that wraps the request in order to override its + * {@link HttpServletRequest#getServerName() getServerName()}, + * {@link HttpServletRequest#getServerPort() getServerPort()}, + * {@link HttpServletRequest#getScheme() getScheme()}, and + * {@link HttpServletRequest#isSecure() isSecure()} methods with values derived + * from "Forwarded" or "X-Forwarded-*" headers. In effect the wrapped request + * reflects the client-originated protocol and address. + * + * @author Rossen Stoyanchev + * @author Eddú Meléndez + * @since 4.3 + */ +public class ForwardedHeaderFilter extends OncePerRequestFilter { + + private static final Set<String> FORWARDED_HEADER_NAMES = + Collections.newSetFromMap(new LinkedCaseInsensitiveMap<Boolean>(5, Locale.ENGLISH)); + + static { + FORWARDED_HEADER_NAMES.add("Forwarded"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Host"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Port"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix"); + } + + + private final UrlPathHelper pathHelper = new UrlPathHelper(); + + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if (FORWARDED_HEADER_NAMES.contains(name)) { + return false; + } + } + return true; + } + + @Override + protected boolean shouldNotFilterAsyncDispatch() { + return false; + } + + @Override + protected boolean shouldNotFilterErrorDispatch() { + return false; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + filterChain.doFilter(new ForwardedHeaderRequestWrapper(request, this.pathHelper), response); + } + + + private static class ForwardedHeaderRequestWrapper extends HttpServletRequestWrapper { + + private final String scheme; + + private final boolean secure; + + private final String host; + + private final int port; + + private final String contextPath; + + private final String requestUri; + + private final StringBuffer requestUrl; + + private final Map<String, List<String>> headers; + + public ForwardedHeaderRequestWrapper(HttpServletRequest request, UrlPathHelper pathHelper) { + super(request); + + HttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + int port = uriComponents.getPort(); + + this.scheme = uriComponents.getScheme(); + this.secure = "https".equals(scheme); + this.host = uriComponents.getHost(); + this.port = (port == -1 ? (this.secure ? 443 : 80) : port); + + String prefix = getForwardedPrefix(request); + this.contextPath = (prefix != null ? prefix : request.getContextPath()); + this.requestUri = this.contextPath + pathHelper.getPathWithinApplication(request); + this.requestUrl = new StringBuffer(this.scheme + "://" + this.host + + (port == -1 ? "" : ":" + port) + this.requestUri); + this.headers = initHeaders(request); + } + + private static String getForwardedPrefix(HttpServletRequest request) { + String prefix = null; + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) { + prefix = request.getHeader(name); + } + } + if (prefix != null) { + while (prefix.endsWith("/")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + } + return prefix; + } + + /** + * Copy the headers excluding any {@link #FORWARDED_HEADER_NAMES}. + */ + private static Map<String, List<String>> initHeaders(HttpServletRequest request) { + Map<String, List<String>> headers = new LinkedCaseInsensitiveMap<List<String>>(Locale.ENGLISH); + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if (!FORWARDED_HEADER_NAMES.contains(name)) { + headers.put(name, Collections.list(request.getHeaders(name))); + } + } + return headers; + } + + @Override + public String getScheme() { + return this.scheme; + } + + @Override + public String getServerName() { + return this.host; + } + + @Override + public int getServerPort() { + return this.port; + } + + @Override + public boolean isSecure() { + return this.secure; + } + + @Override + public String getContextPath() { + return this.contextPath; + } + + @Override + public String getRequestURI() { + return this.requestUri; + } + + @Override + public StringBuffer getRequestURL() { + return this.requestUrl; + } + + // Override header accessors to not expose forwarded headers + + @Override + public String getHeader(String name) { + List<String> value = this.headers.get(name); + return (CollectionUtils.isEmpty(value) ? null : value.get(0)); + } + + @Override + public Enumeration<String> getHeaders(String name) { + List<String> value = this.headers.get(name); + return (Collections.enumeration(value != null ? value : Collections.<String>emptySet())); + } + + @Override + public Enumeration<String> getHeaderNames() { + return Collections.enumeration(this.headers.keySet()); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index b3fbe838..c94791a5 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.web.filter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; + import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; @@ -60,11 +61,28 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING"; - /** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */ private static final boolean servlet3Present = ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class); + private boolean writeWeakETag = false; + + /** + * Set whether the ETag value written to the response should be weak, as per rfc7232. + * <p>Should be configured using an {@code <init-param>} for parameter name + * "writeWeakETag" in the filter definition in {@code web.xml}. + * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">rfc7232 section-2.3</a> + */ + public boolean isWriteWeakETag() { + return writeWeakETag; + } + + /** + * Return whether the ETag value written to the response should be weak, as per rfc7232. + */ + public void setWriteWeakETag(boolean writeWeakETag) { + this.writeWeakETag = writeWeakETag; + } /** * The default value is "false" so that the filter may delay the generation of @@ -102,10 +120,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { responseWrapper.copyBodyToResponse(); } else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) { - String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream()); + String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream(), this.writeWeakETag); rawResponse.setHeader(HEADER_ETAG, responseETag); String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); - if (responseETag.equals(requestETag)) { + if (requestETag != null + && (responseETag.equals(requestETag) + || responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", "")) + || "*".equals(requestETag))) { if (logger.isTraceEnabled()) { logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304"); } @@ -144,7 +165,10 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response, int responseStatusCode, InputStream inputStream) { - if (responseStatusCode >= 200 && responseStatusCode < 300 && HttpMethod.GET.matches(request.getMethod())) { + String method = request.getMethod(); + if (responseStatusCode >= 200 && responseStatusCode < 300 && + (HttpMethod.GET.matches(method) || HttpMethod.HEAD.matches(method))) { + String cacheControl = null; if (servlet3Present) { cacheControl = response.getHeader(HEADER_CACHE_CONTROL); @@ -160,11 +184,17 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { * Generate the ETag header value from the given response body byte array. * <p>The default implementation generates an MD5 hash. * @param inputStream the response body as an InputStream + * @param isWeak whether the generated ETag should be weak * @return the ETag header value * @see org.springframework.util.DigestUtils */ - protected String generateETagHeaderValue(InputStream inputStream) throws IOException { - StringBuilder builder = new StringBuilder("\"0"); + protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException { + // length of W/ + 0 + " + 32bits md5 hash + " + StringBuilder builder = new StringBuilder(37); + if (isWeak) { + builder.append("W/"); + } + builder.append("\"0"); DigestUtils.appendMd5DigestAsHex(inputStream, builder); builder.append('"'); return builder.toString(); diff --git a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java index 0f857952..1567b39b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java +++ b/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.OrderUtils; import org.springframework.util.Assert; @@ -102,7 +103,9 @@ public class ControllerAdviceBean implements Ordered { this.order = initOrderFromBean(bean); } - ControllerAdvice annotation = AnnotationUtils.findAnnotation(beanType, ControllerAdvice.class); + ControllerAdvice annotation = + AnnotatedElementUtils.findMergedAnnotation(beanType, ControllerAdvice.class); + if (annotation != null) { this.basePackages = initBasePackages(annotation); this.assignableTypes = Arrays.asList(annotation.assignableTypes()); diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index 33a9b291..74f99557 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -226,7 +226,7 @@ public class HandlerMethod { * if no annotation can be found on the given method itself. * <p>Also supports <em>merged</em> composed annotations with attribute * overrides as of Spring Framework 4.2.2. - * @param annotationType the type of annotation to introspect the method for. + * @param annotationType the type of annotation to introspect the method for * @return the annotation, or {@code null} if none found * @see AnnotatedElementUtils#findMergedAnnotation */ @@ -235,6 +235,16 @@ public class HandlerMethod { } /** + * Return whether the parameter is declared with the given annotation type. + * @param annotationType the annotation type to look for + * @since 4.3 + * @see AnnotatedElementUtils#hasAnnotation + */ + public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) { + return AnnotatedElementUtils.hasAnnotation(this.method, annotationType); + } + + /** * If the provided instance contains a bean name rather than an object instance, * the bean name is resolved before a {@link HandlerMethod} is created and returned. */ @@ -247,6 +257,15 @@ public class HandlerMethod { return new HandlerMethod(this, handler); } + /** + * Return a short representation of this handler method for log message purposes. + * @since 4.3 + */ + public String getShortLogMessage() { + int args = this.method.getParameterTypes().length; + return getBeanType().getName() + "#" + this.method.getName() + "[" + args + " args]"; + } + @Override public boolean equals(Object other) { @@ -280,6 +299,10 @@ public class HandlerMethod { super(HandlerMethod.this.bridgedMethod, index); } + protected HandlerMethodParameter(HandlerMethodParameter original) { + super(original); + } + @Override public Class<?> getContainingClass() { return HandlerMethod.this.getBeanType(); @@ -289,6 +312,16 @@ public class HandlerMethod { public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) { return HandlerMethod.this.getMethodAnnotation(annotationType); } + + @Override + public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) { + return HandlerMethod.this.hasMethodAnnotation(annotationType); + } + + @Override + public HandlerMethodParameter clone() { + return new HandlerMethodParameter(this); + } } @@ -304,10 +337,20 @@ public class HandlerMethod { this.returnValue = returnValue; } + protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + } + @Override public Class<?> getParameterType() { return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); } + + @Override + public ReturnValueMethodParameter clone() { + return new ReturnValueMethodParameter(this); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java index 82acd2c3..4fc0545b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethodSelector.java @@ -39,7 +39,7 @@ public abstract class HandlerMethodSelector { * @param handlerType the handler type to search handler methods on * @param handlerMethodFilter a {@link MethodFilter} to help recognize handler methods of interest * @return the selected methods, or an empty set - * @see MethodIntrospector#selectMethods(Class, MethodFilter) + * @see MethodIntrospector#selectMethods */ public static Set<Method> selectMethods(Class<?> handlerType, MethodFilter handlerMethodFilter) { return MethodIntrospector.selectMethods(handlerType, handlerMethodFilter); diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java index aa724396..9d938367 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ public abstract class AbstractCookieValueMethodArgumentResolver extends Abstract @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new ServletRequestBindingException("Missing cookie '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java index 78a3a345..e2e92d64 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; +import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -53,6 +54,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 */ public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -61,7 +63,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle private final BeanExpressionContext expressionContext; - private Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<MethodParameter, NamedValueInfo>(256); + private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<MethodParameter, NamedValueInfo>(256); public AbstractNamedValueMethodArgumentResolver() { @@ -84,27 +86,33 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - Class<?> paramType = parameter.getParameterType(); NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); - Object arg = resolveName(namedValueInfo.name, parameter, webRequest); + Object resolvedName = resolveStringValue(namedValueInfo.name); + if (resolvedName == null) { + throw new IllegalArgumentException( + "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); + } + + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); if (arg == null) { if (namedValueInfo.defaultValue != null) { - arg = resolveDefaultValue(namedValueInfo.defaultValue); + arg = resolveStringValue(namedValueInfo.defaultValue); } - else if (namedValueInfo.required && !parameter.getParameterType().getName().equals("java.util.Optional")) { - handleMissingValue(namedValueInfo.name, parameter); + else if (namedValueInfo.required && !nestedParameter.isOptional()) { + handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } - arg = handleNullValue(namedValueInfo.name, arg, paramType); + arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { - arg = resolveDefaultValue(namedValueInfo.defaultValue); + arg = resolveStringValue(namedValueInfo.defaultValue); } if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { - arg = binder.convertIfNecessary(arg, paramType, parameter); + arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), @@ -151,7 +159,8 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle if (info.name.length() == 0) { name = parameter.getParameterName(); if (name == null) { - throw new IllegalArgumentException("Name for argument type [" + parameter.getParameterType().getName() + + throw new IllegalArgumentException( + "Name for argument type [" + parameter.getNestedParameterType().getName() + "] not available, and parameter name information not found in class file either."); } } @@ -160,29 +169,43 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle } /** - * Resolves the given parameter type and value name into an argument value. + * Resolve the given annotation-specified value, + * potentially containing placeholders and expressions. + */ + private Object resolveStringValue(String value) { + if (this.configurableBeanFactory == null) { + return value; + } + String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); + BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); + if (exprResolver == null) { + return value; + } + return exprResolver.evaluate(placeholdersResolved, this.expressionContext); + } + + /** + * Resolve the given parameter type and value name into an argument value. * @param name the name of the value being resolved * @param parameter the method parameter to resolve to an argument value + * (pre-nested in case of a {@link java.util.Optional} declaration) * @param request the current request - * @return the resolved argument. May be {@code null} + * @return the resolved argument (may be {@code null}) * @throws Exception in case of errors */ protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception; /** - * Resolves the given default value into an argument value. + * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)} + * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case. + * @param name the name for the value + * @param parameter the method parameter + * @param request the current request + * @since 4.3 */ - private Object resolveDefaultValue(String defaultValue) { - if (this.configurableBeanFactory == null) { - return defaultValue; - } - String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(defaultValue); - BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); - if (exprResolver == null) { - return defaultValue; - } - return exprResolver.evaluate(placeholdersResolved, this.expressionContext); + protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + handleMissingValue(name, parameter); } /** @@ -191,7 +214,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle * @param name the name for the value * @param parameter the method parameter */ - protected abstract void handleMissingValue(String name, MethodParameter parameter) throws ServletException; + protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { + throw new ServletRequestBindingException("Missing argument '" + name + + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); + } /** * A {@code null} results in a {@code false} value for {@code boolean}s or an exception for other primitives. @@ -202,7 +228,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle return Boolean.FALSE; } else if (paramType.isPrimitive()) { - throw new IllegalStateException("Optional " + paramType + " parameter '" + name + + throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name + "' is present but cannot be translated into a null value due to being declared as a " + "primitive type. Consider declaring it as object wrapper for the corresponding primitive type."); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java index 5d307cba..c121f575 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; * to the exception types supported by a given {@link Method}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 */ public class ExceptionHandlerMethodResolver { @@ -98,8 +99,8 @@ public class ExceptionHandlerMethodResolver { } protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { - ExceptionHandler annot = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); - result.addAll(Arrays.asList(annot.value())); + ExceptionHandler ann = AnnotationUtils.findAnnotation(method, ExceptionHandler.class); + result.addAll(Arrays.asList(ann.value())); } private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) { @@ -124,7 +125,14 @@ public class ExceptionHandlerMethodResolver { * @return a Method to handle the exception, or {@code null} if none found */ public Method resolveMethod(Exception exception) { - return resolveMethodByExceptionType(exception.getClass()); + Method method = resolveMethodByExceptionType(exception.getClass()); + if (method == null) { + Throwable cause = exception.getCause(); + if (cause != null) { + method = resolveMethodByExceptionType(cause.getClass()); + } + } + return method; } /** @@ -133,7 +141,7 @@ public class ExceptionHandlerMethodResolver { * @param exceptionType the exception type * @return a Method to handle the exception, or {@code null} if none found */ - public Method resolveMethodByExceptionType(Class<? extends Exception> exceptionType) { + public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) { Method method = this.exceptionLookupCache.get(exceptionType); if (method == null) { method = getMappedMethod(exceptionType); @@ -145,7 +153,7 @@ public class ExceptionHandlerMethodResolver { /** * Return the {@link Method} mapped to the given exception type, or {@code null} if none. */ - private Method getMappedMethod(Class<? extends Exception> exceptionType) { + private Method getMappedMethod(Class<? extends Throwable> exceptionType) { List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>(); for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { if (mappedException.isAssignableFrom(exceptionType)) { diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index 72e43cd1..8c82eefd 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,23 +38,24 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; /** - * Resolves method arguments annotated with {@code @ModelAttribute} and handles - * return values from methods annotated with {@code @ModelAttribute}. + * Resolve {@code @ModelAttribute} annotated method arguments and handle + * return values from {@code @ModelAttribute} annotated methods. * - * <p>Model attributes are obtained from the model or if not found possibly - * created with a default constructor if it is available. Once created, the - * attributed is populated with request data via data binding and also - * validation may be applied if the argument is annotated with - * {@code @javax.validation.Valid}. + * <p>Model attributes are obtained from the model or created with a default + * constructor (and then added to the model). Once created the attribute is + * populated via data binding to Servlet request parameters. Validation may be + * applied if the argument is annotated with {@code @javax.validation.Valid}. + * or Spring's own {@code @org.springframework.validation.annotation.Validated}. * - * <p>When this handler is created with {@code annotationNotRequired=true}, + * <p>When this handler is created with {@code annotationNotRequired=true} * any non-simple type argument and return value is regarded as a model * attribute with or without the presence of an {@code @ModelAttribute}. * * @author Rossen Stoyanchev * @since 3.1 */ -public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { +public class ModelAttributeMethodProcessor + implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { protected final Log logger = LogFactory.getLog(getClass()); @@ -62,6 +63,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol /** + * Class constructor. * @param annotationNotRequired if "true", non-simple method arguments and * return values are considered model attributes with or without a * {@code @ModelAttribute} annotation. @@ -72,20 +74,14 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol /** - * Returns {@code true} if the parameter is annotated with {@link ModelAttribute} - * or in default resolution mode, and also if it is not a simple type. + * Returns {@code true} if the parameter is annotated with + * {@link ModelAttribute} or, if in default resolution mode, for any + * method parameter that is not a simple type. */ @Override public boolean supportsParameter(MethodParameter parameter) { - if (parameter.hasParameterAnnotation(ModelAttribute.class)) { - return true; - } - else if (this.annotationNotRequired) { - return !BeanUtils.isSimpleProperty(parameter.getParameterType()); - } - else { - return false; - } + return (parameter.hasParameterAnnotation(ModelAttribute.class) || + (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); } /** @@ -102,12 +98,21 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String name = ModelFactory.getNameForParameter(parameter); - Object attribute = (mavContainer.containsAttribute(name) ? - mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest)); + Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) : + createAttribute(name, parameter, binderFactory, webRequest)); + + if (!mavContainer.isBindingDisabled(name)) { + ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); + if (ann != null && !ann.binding()) { + mavContainer.setBindingDisabled(name); + } + } WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { - bindRequestParameters(binder, webRequest); + if (!mavContainer.isBindingDisabled(name)) { + bindRequestParameters(binder, webRequest); + } validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); @@ -182,19 +187,13 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol /** * Return {@code true} if there is a method-level {@code @ModelAttribute} - * or if it is a non-simple type when {@code annotationNotRequired=true}. + * or, in default resolution mode, for any return value type that is not + * a simple type. */ @Override public boolean supportsReturnType(MethodParameter returnType) { - if (returnType.getMethodAnnotation(ModelAttribute.class) != null) { - return true; - } - else if (this.annotationNotRequired) { - return !BeanUtils.isSimpleProperty(returnType.getParameterType()); - } - else { - return false; - } + return (returnType.hasMethodAnnotation(ModelAttribute.class) || + (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType()))); } /** diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java index 9eb31c0d..be4472d3 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java @@ -112,7 +112,7 @@ public final class ModelFactory { if (!container.containsAttribute(name)) { Object value = this.sessionAttributesHandler.retrieveAttribute(request, name); if (value == null) { - throw new HttpSessionRequiredException("Expected session attribute '" + name + "'"); + throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name); } container.addAttribute(name, value); } @@ -128,14 +128,20 @@ public final class ModelFactory { while (!this.modelMethods.isEmpty()) { InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod(); - String modelName = modelMethod.getMethodAnnotation(ModelAttribute.class).value(); - if (container.containsAttribute(modelName)) { + ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class); + if (container.containsAttribute(ann.name())) { + if (!ann.binding()) { + container.setBindingDisabled(ann.name()); + } continue; } Object returnValue = modelMethod.invokeForRequest(request, container); if (!modelMethod.isVoid()){ String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType()); + if (!ann.binding()) { + container.setBindingDisabled(returnValueName); + } if (!container.containsAttribute(returnValueName)) { container.addAttribute(returnValueName, returnValue); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java index 3ef1b4fe..c8575251 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -56,7 +56,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho @Override public boolean supportsParameter(MethodParameter parameter) { return (parameter.hasParameterAnnotation(RequestHeader.class) && - !Map.class.isAssignableFrom(parameter.getParameterType())); + !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())); } @Override @@ -79,7 +79,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new ServletRequestBindingException("Missing request header '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java index c481b152..bc065619 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java @@ -17,22 +17,17 @@ package org.springframework.web.method.annotation; import java.beans.PropertyEditor; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.Part; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.WebDataBinder; @@ -45,6 +40,8 @@ import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.multipart.support.MultipartResolutionDelegate; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.WebUtils; @@ -85,7 +82,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod /** * @param useDefaultResolution in default resolution mode a method argument * that is a simple type, as defined in {@link BeanUtils#isSimpleProperty}, - * is treated as a request parameter even if it it isn't annotated, the + * is treated as a request parameter even if it isn't annotated, the * request parameter name is derived from the method parameter name. */ public RequestParamMethodArgumentResolver(boolean useDefaultResolution) { @@ -98,7 +95,7 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod * values are not expected to contain expressions * @param useDefaultResolution in default resolution mode a method argument * that is a simple type, as defined in {@link BeanUtils#isSimpleProperty}, - * is treated as a request parameter even if it it isn't annotated, the + * is treated as a request parameter even if it isn't annotated, the * request parameter name is derived from the method parameter name. */ public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) { @@ -124,9 +121,8 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod */ @Override public boolean supportsParameter(MethodParameter parameter) { - Class<?> paramType = parameter.getParameterType(); if (parameter.hasParameterAnnotation(RequestParam.class)) { - if (Map.class.isAssignableFrom(paramType)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String paramName = parameter.getParameterAnnotation(RequestParam.class).name(); return StringUtils.hasText(paramName); } @@ -138,11 +134,12 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod if (parameter.hasParameterAnnotation(RequestPart.class)) { return false; } - else if (MultipartFile.class == paramType || "javax.servlet.http.Part".equals(paramType.getName())) { + parameter = parameter.nestedIfOptional(); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { return true; } else if (this.useDefaultResolution) { - return BeanUtils.isSimpleProperty(paramType); + return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); } else { return false; @@ -157,105 +154,53 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod } @Override - protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { - HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); - Object arg; - if (MultipartFile.class == parameter.getParameterType()) { - assertIsMultipartRequest(servletRequest); - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFile(name); + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + return mpArg; } - else if (isMultipartFileCollection(parameter)) { - assertIsMultipartRequest(servletRequest); - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFiles(name); - } - else if (isMultipartFileArray(parameter)) { - assertIsMultipartRequest(servletRequest); - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - List<MultipartFile> multipartFiles = multipartRequest.getFiles(name); - arg = multipartFiles.toArray(new MultipartFile[multipartFiles.size()]); - } - else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { - assertIsMultipartRequest(servletRequest); - arg = servletRequest.getPart(name); - } - else if (isPartCollection(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = new ArrayList<Object>(servletRequest.getParts()); - } - else if (isPartArray(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = RequestPartResolver.resolvePart(servletRequest); - } - else { - arg = null; - if (multipartRequest != null) { - List<MultipartFile> files = multipartRequest.getFiles(name); - if (!files.isEmpty()) { - arg = (files.size() == 1 ? files.get(0) : files); - } + + Object arg = null; + if (multipartRequest != null) { + List<MultipartFile> files = multipartRequest.getFiles(name); + if (!files.isEmpty()) { + arg = (files.size() == 1 ? files.get(0) : files); } - if (arg == null) { - String[] paramValues = webRequest.getParameterValues(name); - if (paramValues != null) { - arg = (paramValues.length == 1 ? paramValues[0] : paramValues); - } + } + if (arg == null) { + String[] paramValues = request.getParameterValues(name); + if (paramValues != null) { + arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } - return arg; } - private void assertIsMultipartRequest(HttpServletRequest request) { - String contentType = request.getContentType(); - if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) { - throw new MultipartException("The current request is not a multipart request"); - } - } - - private boolean isMultipartFileCollection(MethodParameter parameter) { - return (MultipartFile.class == getCollectionParameterType(parameter)); - } - - private boolean isMultipartFileArray(MethodParameter parameter) { - return (MultipartFile.class == parameter.getParameterType().getComponentType()); - } - - private boolean isPartCollection(MethodParameter parameter) { - Class<?> collectionType = getCollectionParameterType(parameter); - return (collectionType != null && "javax.servlet.http.Part".equals(collectionType.getName())); - } - - private boolean isPartArray(MethodParameter parameter) { - Class<?> paramType = parameter.getParameterType().getComponentType(); - return (paramType != null && "javax.servlet.http.Part".equals(paramType.getName())); - } - - private Class<?> getCollectionParameterType(MethodParameter parameter) { - Class<?> paramType = parameter.getParameterType(); - if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ - Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(parameter); - if (valueType != null) { - return valueType; + @Override + protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { + throw new MultipartException("Current request is not a multipart request"); + } + else { + throw new MissingServletRequestPartException(name); } } - return null; - } - - @Override - protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { - throw new MissingServletRequestParameterException(name, parameter.getParameterType().getSimpleName()); + else { + throw new MissingServletRequestParameterException(name, parameter.getNestedParameterType().getSimpleName()); + } } @Override public void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) { - Class<?> paramType = parameter.getParameterType(); + Class<?> paramType = parameter.getNestedParameterType(); if (Map.class.isAssignableFrom(paramType) || MultipartFile.class == paramType || "javax.servlet.http.Part".equals(paramType.getName())) { return; @@ -266,6 +211,11 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod parameter.getParameterName() : requestParam.name()); if (value == null) { + if (requestParam != null) { + if (!requestParam.required() || !requestParam.defaultValue().equals(ValueConstants.DEFAULT_NONE)) { + return; + } + } builder.queryParam(name); } else if (value instanceof Collection) { @@ -306,12 +256,4 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod } } - - private static class RequestPartResolver { - - public static Object resolvePart(HttpServletRequest servletRequest) throws Exception { - return servletRequest.getParts().toArray(new Part[servletRequest.getParts().size()]); - } - } - } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java index 3ff2b315..8cc07e46 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.SessionAttributeStore; @@ -65,10 +65,11 @@ public class SessionAttributesHandler { * @param sessionAttributeStore used for session access */ public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) { - Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null."); + Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null"); this.sessionAttributeStore = sessionAttributeStore; - SessionAttributes annotation = AnnotationUtils.findAnnotation(handlerType, SessionAttributes.class); + SessionAttributes annotation = + AnnotatedElementUtils.findMergedAnnotation(handlerType, SessionAttributes.class); if (annotation != null) { this.attributeNames.addAll(Arrays.asList(annotation.names())); this.attributeTypes.addAll(Arrays.asList(annotation.types())); @@ -84,7 +85,7 @@ public class SessionAttributesHandler { * session attributes through an {@link SessionAttributes} annotation. */ public boolean hasSessionAttributes() { - return ((this.attributeNames.size() > 0) || (this.attributeTypes.size() > 0)); + return (this.attributeNames.size() > 0 || this.attributeTypes.size() > 0); } /** diff --git a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java index 5492582e..e80655a9 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java @@ -34,6 +34,7 @@ import org.springframework.web.context.request.NativeWebRequest; * Previously resolved method parameters are cached for faster lookups. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 */ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { @@ -57,6 +58,19 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu /** * Add the given {@link HandlerMethodArgumentResolver}s. + * @since 4.3 + */ + public HandlerMethodArgumentResolverComposite addResolvers(HandlerMethodArgumentResolver... resolvers) { + if (resolvers != null) { + for (HandlerMethodArgumentResolver resolver : resolvers) { + this.argumentResolvers.add(resolver); + } + } + return this; + } + + /** + * Add the given {@link HandlerMethodArgumentResolver}s. */ public HandlerMethodArgumentResolverComposite addResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) { if (resolvers != null) { @@ -74,6 +88,14 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu return Collections.unmodifiableList(this.argumentResolvers); } + /** + * Clear the list of configured resolvers. + * @since 4.3 + */ + public void clear() { + this.argumentResolvers.clear(); + } + /** * Whether the given {@linkplain MethodParameter method parameter} is supported by any registered diff --git a/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java b/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java index 536d53b0..ec976824 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package org.springframework.web.method.support; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import org.springframework.http.HttpStatus; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.validation.support.BindingAwareModelMap; @@ -54,6 +57,11 @@ public class ModelAndViewContainer { private boolean redirectModelScenario = false; + /* Names of attributes with binding disabled */ + private final Set<String> bindingDisabledAttributes = new HashSet<String>(4); + + private HttpStatus status; + private final SessionStatus sessionStatus = new SimpleSessionStatus(); private boolean requestHandled = false; @@ -134,6 +142,24 @@ public class ModelAndViewContainer { } /** + * Register an attribute for which data binding should not occur, for example + * corresponding to an {@code @ModelAttribute(binding=false)} declaration. + * @param attributeName the name of the attribute + * @since 4.3 + */ + public void setBindingDisabled(String attributeName) { + this.bindingDisabledAttributes.add(attributeName); + } + + /** + * Whether binding is disabled for the given model attribute. + * @since 4.3 + */ + public boolean isBindingDisabled(String name) { + return this.bindingDisabledAttributes.contains(name); + } + + /** * Whether to use the default model or the redirect model. */ private boolean useDefaultModel() { @@ -156,7 +182,7 @@ public class ModelAndViewContainer { /** * Provide a separate model instance to use in a redirect scenario. - * The provided additional model however is not used used unless + * The provided additional model however is not used unless * {@link #setRedirectModelScenario(boolean)} gets set to {@code true} to signal * a redirect scenario. */ @@ -181,6 +207,23 @@ public class ModelAndViewContainer { } /** + * Provide a HTTP status that will be passed on to with the + * {@code ModelAndView} used for view rendering purposes. + * @since 4.3 + */ + public void setStatus(HttpStatus status) { + this.status = status; + } + + /** + * Return the configured HTTP status, if any. + * @since 4.3 + */ + public HttpStatus getStatus() { + return this.status; + } + + /** * Whether the request has been handled fully within the handler, e.g. * {@code @ResponseBody} method, and therefore view resolution is not * necessary. This flag can also be set when controller methods declare an diff --git a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java index 622b1844..727bc92c 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsFileUploadSupport.java @@ -304,7 +304,7 @@ public abstract class CommonsFileUploadSupport { return defaultEncoding; } MediaType contentType = MediaType.parseMediaType(contentTypeHeader); - Charset charset = contentType.getCharSet(); + Charset charset = contentType.getCharset(); return (charset != null ? charset.name() : defaultEncoding); } diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java index b9ac26ed..577b4e3b 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,10 @@ import org.springframework.web.multipart.MultipartResolver; * <p>If no MultipartResolver bean is found, this filter falls back to a default * MultipartResolver: {@link StandardServletMultipartResolver} for Servlet 3.0, * based on a multipart-config section in {@code web.xml}. + * Note however that at present the Servlet specification only defines how to + * enable multipart configuration on a Servlet and as a result multipart request + * processing is likely not possible in a Filter unless the Servlet container + * provides a workaround such as Tomcat's "allowCasualMultipartParsing" property. * * <p>MultipartResolver lookup is customizable: Override this filter's * {@code lookupMultipartResolver} method to use a custom MultipartResolver diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java new file mode 100644 index 00000000..42248276 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.multipart.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; + +import org.springframework.core.GenericCollectionTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.util.ClassUtils; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.util.WebUtils; + +/** + * A common delegate for {@code HandlerMethodArgumentResolver} implementations + * which need to resolve {@link MultipartFile} and {@link Part} arguments. + * + * @author Juergen Hoeller + * @since 4.3 + */ +public abstract class MultipartResolutionDelegate { + + public static final Object UNRESOLVABLE = new Object(); + + + private static Class<?> servletPartClass = null; + + static { + try { + servletPartClass = ClassUtils.forName("javax.servlet.http.Part", + MultipartResolutionDelegate.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Servlet 3.0 javax.servlet.http.Part type not available - + // Part references simply not supported then. + } + } + + + public static boolean isMultipartRequest(HttpServletRequest request) { + return (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null || + isMultipartContent(request)); + } + + private static boolean isMultipartContent(HttpServletRequest request) { + String contentType = request.getContentType(); + return (contentType != null && contentType.toLowerCase().startsWith("multipart/")); + } + + static MultipartHttpServletRequest asMultipartHttpServletRequest(HttpServletRequest request) { + MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + if (unwrapped != null) { + return unwrapped; + } + return adaptToMultipartHttpServletRequest(request); + } + + private static MultipartHttpServletRequest adaptToMultipartHttpServletRequest(HttpServletRequest request) { + if (servletPartClass != null) { + // Servlet 3.0 available .. + return new StandardMultipartHttpServletRequest(request); + } + throw new MultipartException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); + } + + + public static boolean isMultipartArgument(MethodParameter parameter) { + Class<?> paramType = parameter.getNestedParameterType(); + return (MultipartFile.class == paramType || + isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) || + (servletPartClass != null && (servletPartClass == paramType || + isPartCollection(parameter) || isPartArray(parameter)))); + } + + public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) + throws Exception { + + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); + + if (MultipartFile.class == parameter.getNestedParameterType()) { + if (multipartRequest == null && isMultipart) { + multipartRequest = adaptToMultipartHttpServletRequest(request); + } + return (multipartRequest != null ? multipartRequest.getFile(name) : null); + } + else if (isMultipartFileCollection(parameter)) { + if (multipartRequest == null && isMultipart) { + multipartRequest = adaptToMultipartHttpServletRequest(request); + } + return (multipartRequest != null ? multipartRequest.getFiles(name) : null); + } + else if (isMultipartFileArray(parameter)) { + if (multipartRequest == null && isMultipart) { + multipartRequest = adaptToMultipartHttpServletRequest(request); + } + if (multipartRequest != null) { + List<MultipartFile> multipartFiles = multipartRequest.getFiles(name); + return multipartFiles.toArray(new MultipartFile[multipartFiles.size()]); + } + else { + return null; + } + } + else if (servletPartClass != null) { + if (servletPartClass == parameter.getNestedParameterType()) { + return (isMultipart ? RequestPartResolver.resolvePart(request, name) : null); + } + else if (isPartCollection(parameter)) { + return (isMultipart ? RequestPartResolver.resolvePartList(request, name) : null); + } + else if (isPartArray(parameter)) { + return (isMultipart ? RequestPartResolver.resolvePartArray(request, name) : null); + } + } + return UNRESOLVABLE; + } + + private static boolean isMultipartFileCollection(MethodParameter methodParam) { + return (MultipartFile.class == getCollectionParameterType(methodParam)); + } + + private static boolean isMultipartFileArray(MethodParameter methodParam) { + return (MultipartFile.class == methodParam.getNestedParameterType().getComponentType()); + } + + private static boolean isPartCollection(MethodParameter methodParam) { + return (servletPartClass == getCollectionParameterType(methodParam)); + } + + private static boolean isPartArray(MethodParameter methodParam) { + return (servletPartClass == methodParam.getNestedParameterType().getComponentType()); + } + + private static Class<?> getCollectionParameterType(MethodParameter methodParam) { + Class<?> paramType = methodParam.getNestedParameterType(); + if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ + Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam); + if (valueType != null) { + return valueType; + } + } + return null; + } + + + /** + * Inner class to avoid hard-coded dependency on Servlet 3.0 Part type... + */ + private static class RequestPartResolver { + + public static Object resolvePart(HttpServletRequest servletRequest, String name) throws Exception { + return servletRequest.getPart(name); + } + + public static Object resolvePartList(HttpServletRequest servletRequest, String name) throws Exception { + Collection<Part> parts = servletRequest.getParts(); + List<Part> result = new ArrayList<Part>(parts.size()); + for (Part part : parts) { + if (part.getName().equals(name)) { + result.add(part); + } + } + return result; + } + + public static Object resolvePartArray(HttpServletRequest servletRequest, String name) throws Exception { + Collection<Part> parts = servletRequest.getParts(); + List<Part> result = new ArrayList<Part>(parts.size()); + for (Part part : parts) { + if (part.getName().equals(name)) { + result.add(part); + } + } + return result.toArray(new Part[result.size()]); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java index 78eab918..ae817e4b 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java @@ -26,12 +26,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.util.ClassUtils; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; -import org.springframework.web.util.WebUtils; /** * {@link ServerHttpRequest} implementation that accesses one part of a multipart @@ -57,40 +55,20 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques * @param request the current servlet request * @param partName the name of the part to adapt to the {@link ServerHttpRequest} contract * @throws MissingServletRequestPartException if the request part cannot be found - * @throws IllegalArgumentException if MultipartHttpServletRequest cannot be initialized + * @throws MultipartException if MultipartHttpServletRequest cannot be initialized */ public RequestPartServletServerHttpRequest(HttpServletRequest request, String partName) throws MissingServletRequestPartException { super(request); - this.multipartRequest = asMultipartRequest(request); + this.multipartRequest = MultipartResolutionDelegate.asMultipartHttpServletRequest(request); this.partName = partName; this.headers = this.multipartRequest.getMultipartHeaders(this.partName); if (this.headers == null) { - if (request instanceof MultipartHttpServletRequest) { - throw new MissingServletRequestPartException(partName); - } - else { - throw new IllegalArgumentException( - "Failed to obtain request part: " + partName + ". " + - "The part is missing or multipart processing is not configured. " + - "Check for a MultipartResolver bean or if Servlet 3.0 multipart processing is enabled."); - } - } - } - - private static MultipartHttpServletRequest asMultipartRequest(HttpServletRequest request) { - MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); - if (unwrapped != null) { - return unwrapped; - } - else if (ClassUtils.hasMethod(HttpServletRequest.class, "getParts")) { - // Servlet 3.0 available .. - return new StandardMultipartHttpServletRequest(request); + throw new MissingServletRequestPartException(partName); } - throw new IllegalArgumentException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); } @@ -125,7 +103,7 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques private String determineEncoding() { MediaType contentType = getHeaders().getContentType(); if (contentType != null) { - Charset charset = contentType.getCharSet(); + Charset charset = contentType.getCharset(); if (charset != null) { return charset.name(); } diff --git a/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java new file mode 100644 index 00000000..1323bc94 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/AbstractUriTemplateHandler.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link UriTemplateHandler} implementations. + * + * <p>Support {@link #setBaseUrl} and {@link #setDefaultUriVariables} properties + * that should be relevant regardless of the URI template expand and encode + * mechanism used in sub-classes. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class AbstractUriTemplateHandler implements UriTemplateHandler { + + private String baseUrl; + + private final Map<String, Object> defaultUriVariables = new HashMap<String, Object>(); + + + /** + * Configure a base URL to prepend URI templates with. The base URL must + * have a scheme and host but may optionally contain a port and a path. + * The base URL must be fully expanded and encoded which can be done via + * {@link UriComponentsBuilder}. + * @param baseUrl the base URL. + */ + public void setBaseUrl(String baseUrl) { + if (baseUrl != null) { + UriComponents uriComponents = UriComponentsBuilder.fromUriString(baseUrl).build(); + Assert.hasText(uriComponents.getScheme(), "'baseUrl' must have a scheme"); + Assert.hasText(uriComponents.getHost(), "'baseUrl' must have a host"); + Assert.isNull(uriComponents.getQuery(), "'baseUrl' cannot have a query"); + Assert.isNull(uriComponents.getFragment(), "'baseUrl' cannot have a fragment"); + } + this.baseUrl = baseUrl; + } + + /** + * Return the configured base URL. + */ + public String getBaseUrl() { + return this.baseUrl; + } + + /** + * Configure default URI variable values to use with every expanded URI + * template. These default values apply only when expanding with a Map, and + * not with an array, where the Map supplied to {@link #expand(String, Map)} + * can override the default values. + * @param defaultUriVariables the default URI variable values + * @since 4.3 + */ + public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) { + this.defaultUriVariables.clear(); + if (defaultUriVariables != null) { + this.defaultUriVariables.putAll(defaultUriVariables); + } + } + + /** + * Return a read-only copy of the configured default URI variables. + */ + public Map<String, ?> getDefaultUriVariables() { + return Collections.unmodifiableMap(this.defaultUriVariables); + } + + + @Override + public URI expand(String uriTemplate, Map<String, ?> uriVariables) { + if (!getDefaultUriVariables().isEmpty()) { + Map<String, Object> map = new HashMap<String, Object>(); + map.putAll(getDefaultUriVariables()); + map.putAll(uriVariables); + uriVariables = map; + } + URI url = expandInternal(uriTemplate, uriVariables); + return insertBaseUrl(url); + } + + @Override + public URI expand(String uriTemplate, Object... uriVariables) { + URI url = expandInternal(uriTemplate, uriVariables); + return insertBaseUrl(url); + } + + + /** + * Actually expand and encode the URI template. + */ + protected abstract URI expandInternal(String uriTemplate, Map<String, ?> uriVariables); + + /** + * Actually expand and encode the URI template. + */ + protected abstract URI expandInternal(String uriTemplate, Object... uriVariables); + + + /** + * Insert a base URL (if configured) unless the given URL has a host already. + */ + private URI insertBaseUrl(URI url) { + try { + String baseUrl = getBaseUrl(); + if (baseUrl != null && url.getHost() == null) { + url = new URI(baseUrl + url.toString()); + } + return url; + } + catch (URISyntaxException ex) { + throw new IllegalArgumentException("Invalid URL after inserting base URL: " + url, ex); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java index 7bc23949..214b97ad 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java +++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java @@ -138,7 +138,7 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper { // Overrides Servlet 3.1 setContentLengthLong(long) at runtime public void setContentLengthLong(long len) { if (len > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Content-Length exceeds ShallowEtagHeaderFilter's maximum (" + + throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" + Integer.MAX_VALUE + "): " + len); } int lenInt = (int) len; diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java index e4f79832..1116df9e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,62 +16,39 @@ package org.springframework.web.util; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.List; import java.util.Map; -import org.springframework.util.Assert; - /** - * Default implementation of {@link UriTemplateHandler} that relies on - * {@link UriComponentsBuilder} internally. + * Default implementation of {@link UriTemplateHandler} based on the use of + * {@link UriComponentsBuilder} for expanding and encoding variables. + * + * <p>There are also several properties to customize how URI template handling + * is performed, including a {@link #setBaseUrl baseUrl} to be used as a prefix + * for all URI templates and a couple of encoding related options — + * {@link #setParsePath parsePath} and {@link #setStrictEncoding strictEncoding} + * respectively. * * @author Rossen Stoyanchev * @since 4.2 */ -public class DefaultUriTemplateHandler implements UriTemplateHandler { - - private String baseUrl; +public class DefaultUriTemplateHandler extends AbstractUriTemplateHandler { private boolean parsePath; + private boolean strictEncoding; - /** - * Configure a base URL to prepend URI templates with. The base URL should - * have a scheme and host but may also contain a port and a partial path. - * Individual URI templates then may provide the remaining part of the URL - * including additional path, query and fragment. - * <p><strong>Note: </strong>Individual URI templates are expanded and - * encoded before being appended to the base URL. Therefore the base URL is - * expected to be fully expanded and encoded, which can be done with the help - * of {@link UriComponentsBuilder}. - * @param baseUrl the base URL. - */ - public void setBaseUrl(String baseUrl) { - if (baseUrl != null) { - UriComponents uriComponents = UriComponentsBuilder.fromUriString(baseUrl).build(); - Assert.hasText(uriComponents.getScheme(), "'baseUrl' must have a scheme"); - Assert.hasText(uriComponents.getHost(), "'baseUrl' must have a host"); - Assert.isNull(uriComponents.getQuery(), "'baseUrl' cannot have a query"); - Assert.isNull(uriComponents.getFragment(), "'baseUrl' cannot have a fragment"); - } - this.baseUrl = baseUrl; - } - - /** - * Return the configured base URL. - */ - public String getBaseUrl() { - return this.baseUrl; - } /** * Whether to parse the path of a URI template string into path segments. - * <p>If set to {@code true} the path of parsed URI templates is decomposed - * into path segments so that URI variables expanded into the path are - * treated according to path segment encoding rules. In effect that means the - * "/" character is percent encoded. + * <p>If set to {@code true} the URI template path is immediately decomposed + * into path segments any URI variables expanded into it are then subject to + * path segment encoding rules. In effect URI variables in the path have any + * "/" characters percent encoded. * <p>By default this is set to {@code false} in which case the path is kept * as a full path and expanded URI variables will preserve "/" characters. * @param parsePath whether to parse the path into path segments @@ -87,24 +64,55 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { return this.parsePath; } + /** + * Whether to encode characters outside the unreserved set as defined in + * <a href="https://tools.ietf.org/html/rfc3986#section-2">RFC 3986 Section 2</a>. + * This ensures a URI variable value will not contain any characters with a + * reserved purpose. + * <p>By default this is set to {@code false} in which case only characters + * illegal for the given URI component are encoded. For example when expanding + * a URI variable into a path segment the "/" character is illegal and + * encoded. The ";" character however is legal and not encoded even though + * it has a reserved purpose. + * <p><strong>Note:</strong> this property supersedes the need to also set + * the {@link #setParsePath parsePath} property. + * @param strictEncoding whether to perform strict encoding + * @since 4.3 + */ + public void setStrictEncoding(boolean strictEncoding) { + this.strictEncoding = strictEncoding; + } + + /** + * Whether to strictly encode any character outside the unreserved set. + */ + public boolean isStrictEncoding() { + return this.strictEncoding; + } + @Override - public URI expand(String uriTemplate, Map<String, ?> uriVariables) { + protected URI expandInternal(String uriTemplate, Map<String, ?> uriVariables) { UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); - UriComponents uriComponents = uriComponentsBuilder.build().expand(uriVariables).encode(); - return insertBaseUrl(uriComponents); + UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables); + return createUri(uriComponents); } @Override - public URI expand(String uriTemplate, Object... uriVariableValues) { + protected URI expandInternal(String uriTemplate, Object... uriVariables) { UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); - UriComponents uriComponents = uriComponentsBuilder.build().expand(uriVariableValues).encode(); - return insertBaseUrl(uriComponents); + UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables); + return createUri(uriComponents); } + /** + * Create a {@code UriComponentsBuilder} from the URI template string. + * This implementation also breaks up the path into path segments depending + * on whether {@link #setParsePath parsePath} is enabled. + */ protected UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate); - if (shouldParsePath()) { + if (shouldParsePath() && !isStrictEncoding()) { List<String> pathSegments = builder.build().getPathSegments(); builder.replacePath(null); for (String pathSegment : pathSegments) { @@ -114,16 +122,50 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { return builder; } - protected URI insertBaseUrl(UriComponents uriComponents) { - if (getBaseUrl() == null || uriComponents.getHost() != null) { - return uriComponents.toUri(); + protected UriComponents expandAndEncode(UriComponentsBuilder builder, Map<String, ?> uriVariables) { + if (!isStrictEncoding()) { + return builder.buildAndExpand(uriVariables).encode(); + } + else { + Map<String, Object> encodedUriVars = new HashMap<String, Object>(uriVariables.size()); + for (Map.Entry<String, ?> entry : uriVariables.entrySet()) { + encodedUriVars.put(entry.getKey(), applyStrictEncoding(entry.getValue())); + } + return builder.buildAndExpand(encodedUriVars); } - String url = getBaseUrl() + uriComponents.toUriString(); + } + + protected UriComponents expandAndEncode(UriComponentsBuilder builder, Object[] uriVariables) { + if (!isStrictEncoding()) { + return builder.buildAndExpand(uriVariables).encode(); + } + else { + Object[] encodedUriVars = new Object[uriVariables.length]; + for (int i = 0; i < uriVariables.length; i++) { + encodedUriVars[i] = applyStrictEncoding(uriVariables[i]); + } + return builder.buildAndExpand(encodedUriVars); + } + } + + private String applyStrictEncoding(Object value) { + String stringValue = (value != null ? value.toString() : ""); + try { + return UriUtils.encode(stringValue, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + // Should never happen + throw new IllegalStateException("Failed to encode URI variable", ex); + } + } + + private URI createUri(UriComponents uriComponents) { try { - return new URI(url); + // Avoid further encoding (in the case of strictEncoding=true) + return new URI(uriComponents.toUriString()); } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Invalid URL after inserting base URL: " + url, ex); + throw new IllegalStateException("Could not create URI object: " + ex.getMessage(), ex); } } diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index e25466ec..2aceca89 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -61,6 +61,7 @@ final class HierarchicalUriComponents extends UriComponents { private final boolean encoded; + /** * Package-private constructor. All arguments are optional, and can be {@code null}. * @param scheme the scheme @@ -336,6 +337,7 @@ final class HierarchicalUriComponents extends UriComponents { private MultiValueMap<String, String> expandQueryParams(UriTemplateVariables variables) { int size = this.queryParams.size(); MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(size); + variables = new QueryUriTemplateVariables(variables); for (Map.Entry<String, List<String>> entry : this.queryParams.entrySet()) { String name = expandUriComponent(entry.getKey(), variables); List<String> values = new ArrayList<String>(entry.getValue().size()); @@ -882,4 +884,23 @@ final class HierarchicalUriComponents extends UriComponents { } }; + + private static class QueryUriTemplateVariables implements UriTemplateVariables { + + private final UriTemplateVariables delegate; + + public QueryUriTemplateVariables(UriTemplateVariables delegate) { + this.delegate = delegate; + } + + @Override + public Object getValue(String name) { + Object value = this.delegate.getValue(name); + if (ObjectUtils.isArray(value)) { + value = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(value)); + } + return value; + } + } + } diff --git a/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java b/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java index cb43aa9e..3546efd1 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/HtmlUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,6 @@ import org.springframework.util.Assert; * @author Martin Kersten * @author Craig Andrews * @since 01.03.2003 - * @see org.apache.commons.lang.StringEscapeUtils */ public abstract class HtmlUtils { @@ -75,7 +74,7 @@ public abstract class HtmlUtils { * http://www.w3.org/TR/html4/sgml/entities.html * </a> * @param input the (unescaped) input string - * @param encoding The name of a supported {@link java.nio.charset.Charset charset} + * @param encoding the name of a supported {@link java.nio.charset.Charset charset} * @return the escaped string * @since 4.1.2 */ @@ -126,7 +125,7 @@ public abstract class HtmlUtils { * http://www.w3.org/TR/html4/sgml/entities.html * </a> * @param input the (unescaped) input string - * @param encoding The name of a supported {@link java.nio.charset.Charset charset} + * @param encoding the name of a supported {@link java.nio.charset.Charset charset} * @return the escaped string * @since 4.1.2 */ @@ -178,7 +177,7 @@ public abstract class HtmlUtils { * http://www.w3.org/TR/html4/sgml/entities.html * </a> * @param input the (unescaped) input string - * @param encoding The name of a supported {@link java.nio.charset.Charset charset} + * @param encoding the name of a supported {@link java.nio.charset.Charset charset} * @return the escaped string * @since 4.1.2 */ diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java index 43d60943..659030af 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java @@ -105,7 +105,7 @@ public class UriTemplate implements Serializable { * <p>Example: * <pre class="code"> * UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); - * System.out.println(template.expand("Rest & Relax", "42)); + * System.out.println(template.expand("Rest & Relax", 42)); * </pre> * will print: <blockquote>{@code http://example.com/hotels/Rest%20%26%20Relax/bookings/42}</blockquote> * @param uriVariableValues the array of URI variables diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java index ac04e31e..c03eb191 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,15 +20,23 @@ import java.net.URI; import java.util.Map; /** - * A strategy for expanding a URI template with URI variables into a {@link URI}. + * Strategy for expanding a URI template with full control over the URI template + * syntax and the encoding of variables. Also a convenient central point for + * pre-processing all URI templates for example to insert a common base path. + * + * <p>Supported as a property on the {@code RestTemplate} as well as the + * {@code AsyncRestTemplate}. The {@link DefaultUriTemplateHandler} is built + * on Spring's URI template support via {@link UriComponentsBuilder}. An + * alternative implementation may be used to plug external URI template libraries. * * @author Rossen Stoyanchev * @since 4.2 + * @see org.springframework.web.client.RestTemplate#setUriTemplateHandler */ public interface UriTemplateHandler { /** - * Expand the give URI template with a map of URI variables. + * Expand the given URI template from a map of URI variables. * @param uriTemplate the URI template string * @param uriVariables the URI variables * @return the resulting URI @@ -36,11 +44,11 @@ public interface UriTemplateHandler { URI expand(String uriTemplate, Map<String, ?> uriVariables); /** - * Expand the give URI template with an array of URI variable values. + * Expand the given URI template from an array of URI variables. * @param uriTemplate the URI template string - * @param uriVariableValues the URI variable values + * @param uriVariables the URI variable values * @return the resulting URI */ - URI expand(String uriTemplate, Object... uriVariableValues); + URI expand(String uriTemplate, Object... uriVariables); } diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java index c6809297..6b2f3e50 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.util.Assert; * </ul> * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 3.0 * @see <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a> */ @@ -213,4 +214,28 @@ public abstract class UriUtils { return (changed ? new String(bos.toByteArray(), encoding) : source); } + /** + * Extract the file extension from the given URI path. + * @param path the URI path (e.g. "/products/index.html") + * @return the extracted file extension (e.g. "html") + * @since 4.3.2 + */ + public static String extractFileExtension(String path) { + int end = path.indexOf('?'); + if (end == -1) { + end = path.indexOf('#'); + if (end == -1) { + end = path.length(); + } + } + int begin = path.lastIndexOf('/', end) + 1; + int paramIndex = path.indexOf(';', begin); + end = (paramIndex != -1 && paramIndex < end ? paramIndex : end); + int extIndex = path.lastIndexOf('.', end); + if (extIndex != -1 && extIndex > begin) { + return path.substring(extIndex + 1, end); + } + return null; + } + } diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 1a7a4fe3..a1cd6bf6 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -179,8 +179,8 @@ public class UrlPathHelper { String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp); String path; - // if the app container sanitized the servletPath, check against the sanitized version - if (servletPath.indexOf(sanitizedPathWithinApp) != -1) { + // If the app container sanitized the servletPath, check against the sanitized version + if (servletPath.contains(sanitizedPathWithinApp)) { path = getRemainingPath(sanitizedPathWithinApp, servletPath, false); } else { @@ -485,8 +485,8 @@ public class UrlPathHelper { * @return the updated URI string */ public String removeSemicolonContent(String requestUri) { - return this.removeSemicolonContent ? - removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri); + return (this.removeSemicolonContent ? + removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri)); } private String removeSemicolonContentInternal(String requestUri) { diff --git a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java index 6d8062fb..13195a76 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -278,7 +278,6 @@ public abstract class WebUtils { return realPath; } - /** * Determine the session id of the given request, if any. * @param request current HTTP request @@ -353,7 +352,9 @@ public abstract class WebUtils { * @param clazz the class to instantiate for a new attribute * @return the value of the session attribute, newly created if not found * @throws IllegalArgumentException if the session attribute could not be instantiated + * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes */ + @Deprecated public static Object getOrCreateSessionAttribute(HttpSession session, String name, Class<?> clazz) throws IllegalArgumentException { @@ -527,7 +528,9 @@ public abstract class WebUtils { * and the values as corresponding attribute values. Keys need to be Strings. * @param request current HTTP request * @param attributes the attributes Map + * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes */ + @Deprecated public static void exposeRequestAttributes(ServletRequest request, Map<String, ?> attributes) { Assert.notNull(request, "Request must not be null"); Assert.notNull(attributes, "Attributes Map must not be null"); @@ -689,7 +692,9 @@ public abstract class WebUtils { * @param currentPage the current page, to be returned as fallback * if no target page specified * @return the page specified in the request, or current page if not found + * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes */ + @Deprecated public static int getTargetPage(ServletRequest request, String paramPrefix, int currentPage) { Enumeration<String> paramNames = request.getParameterNames(); while (paramNames.hasMoreElements()) { @@ -713,7 +718,9 @@ public abstract class WebUtils { * Correctly resolves nested paths such as "/products/view.html" as well. * @param urlPath the request URL path (e.g. "/index.html") * @return the extracted URI filename (e.g. "index") + * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes */ + @Deprecated public static String extractFilenameFromUrlPath(String urlPath) { String filename = extractFullFilenameFromUrlPath(urlPath); int dotIndex = filename.lastIndexOf('.'); @@ -729,7 +736,10 @@ public abstract class WebUtils { * "/products/view.html" and remove any path and or query parameters. * @param urlPath the request URL path (e.g. "/products/index.html") * @return the extracted URI filename (e.g. "index.html") + * @deprecated as of Spring 4.3.2, in favor of custom code for such purposes + * (or {@link UriUtils#extractFileExtension} for the file extension use case) */ + @Deprecated public static String extractFullFilenameFromUrlPath(String urlPath) { int end = urlPath.indexOf('?'); if (end == -1) { |