diff options
Diffstat (limited to 'spring-web/src')
180 files changed, 6915 insertions, 1631 deletions
diff --git a/spring-web/src/main/java/org/springframework/http/CacheControl.java b/spring-web/src/main/java/org/springframework/http/CacheControl.java index a291e7ae..c9933344 100644 --- a/spring-web/src/main/java/org/springframework/http/CacheControl.java +++ b/spring-web/src/main/java/org/springframework/http/CacheControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,10 @@ public class CacheControl { private boolean proxyRevalidate = false; + private long staleWhileRevalidate = -1; + + private long staleIfError = -1; + private long sMaxAge = -1; @@ -110,7 +114,7 @@ public class CacheControl { * clients sending conditional requests (with "ETag", "If-Modified-Since" headers) and the server responding * with "304 - Not Modified" status. * <p>In order to disable caching and minimize requests/responses exchanges, the {@link #noStore()} directive - * should be used. + * should be used instead of {@link #noCache()}. * @return {@code this}, to facilitate method chaining * @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a> */ @@ -207,6 +211,36 @@ public class CacheControl { return this; } + /** + * Add a "stale-while-revalidate" directive. + * <p>This directive indicates that caches MAY serve the response in + * which it appears after it becomes stale, up to the indicated number of seconds. + * If a cached response is served stale due to the presence of this extension, + * the cache SHOULD attempt to revalidate it while still serving stale responses (i.e., without blocking). + * @param staleWhileRevalidate the maximum time the response should be used while being revalidated + * @param unit the time unit of the {@code staleWhileRevalidate} argument + * @return {@code this}, to facilitate method chaining + * @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a> + */ + public CacheControl staleWhileRevalidate(long staleWhileRevalidate, TimeUnit unit) { + this.staleWhileRevalidate = unit.toSeconds(staleWhileRevalidate); + return this; + } + + /** + * Add a "stale-if-error" directive. + * <p>This directive indicates that when an error is encountered, a cached stale response MAY be used to satisfy + * the request, regardless of other freshness information. + * @param staleIfError the maximum time the response should be used when errors are encountered + * @param unit the time unit of the {@code staleIfError} argument + * @return {@code this}, to facilitate method chaining + * @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a> + */ + public CacheControl staleIfError(long staleIfError, TimeUnit unit) { + this.staleIfError = unit.toSeconds(staleIfError); + return this; + } + /** * Return the "Cache-Control" header value. @@ -241,6 +275,13 @@ public class CacheControl { if (this.sMaxAge != -1) { appendDirective(ccValue, "s-maxage=" + Long.toString(this.sMaxAge)); } + if (this.staleIfError != -1) { + appendDirective(ccValue, "stale-if-error=" + Long.toString(this.staleIfError)); + } + if (this.staleWhileRevalidate != -1) { + appendDirective(ccValue, "stale-while-revalidate=" + Long.toString(this.staleWhileRevalidate)); + } + String ccHeaderValue = ccValue.toString(); return (StringUtils.hasText(ccHeaderValue) ? ccHeaderValue : null); } diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 1170bb06..0ba9a02a 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -34,6 +34,8 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -55,6 +57,8 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel + * @author Juergen Hoeller * @since 3.0 */ public class HttpHeaders implements MultiValueMap<String, String>, Serializable { @@ -372,6 +376,12 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable "EEE MMM dd HH:mm:ss yyyy" }; + /** + * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match" + * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> + */ + private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); @@ -419,19 +429,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * <p>Returns an empty list when the acceptable media types are unspecified. */ public List<MediaType> getAccept() { - String value = getFirst(ACCEPT); - List<MediaType> result = (value != null ? MediaType.parseMediaTypes(value) : Collections.<MediaType>emptyList()); - - // Some containers parse 'Accept' into multiple values - if (result.size() == 1) { - List<String> acceptHeader = get(ACCEPT); - if (acceptHeader.size() > 1) { - value = StringUtils.collectionToCommaDelimitedString(acceptHeader); - result = MediaType.parseMediaTypes(value); - } - } - - return result; + return MediaType.parseMediaTypes(get(ACCEPT)); } /** @@ -442,10 +440,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Allow-Credentials} response header. + * Return the value of the {@code Access-Control-Allow-Credentials} response header. */ public boolean getAccessControlAllowCredentials() { - return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); + return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); } /** @@ -456,10 +454,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Allow-Headers} response header. + * Return the value of the {@code Access-Control-Allow-Headers} response header. */ public List<String> getAccessControlAllowHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); + return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS); } /** @@ -476,7 +474,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable List<HttpMethod> result = new ArrayList<HttpMethod>(); String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); if (value != null) { - String[] tokens = value.split(",\\s*"); + String[] tokens = StringUtils.tokenizeToStringArray(value, ",", true, true); for (String token : tokens) { HttpMethod resolved = HttpMethod.resolve(token); if (resolved != null) { @@ -498,7 +496,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * Return the value of the {@code Access-Control-Allow-Origin} response header. */ public String getAccessControlAllowOrigin() { - return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); + return getFieldValues(ACCESS_CONTROL_ALLOW_ORIGIN); } /** @@ -509,10 +507,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Expose-Headers} response header. + * Return the value of the {@code Access-Control-Expose-Headers} response header. */ public List<String> getAccessControlExposeHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); + return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS); } /** @@ -523,7 +521,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Max-Age} response header. + * Return the value of the {@code Access-Control-Max-Age} response header. * <p>Returns -1 when the max age is unknown. */ public long getAccessControlMaxAge() { @@ -539,10 +537,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Returns the value of the {@code Access-Control-Request-Headers} request header. + * Return the value of the {@code Access-Control-Request-Headers} request header. */ public List<String> getAccessControlRequestHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); + return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS); } /** @@ -643,7 +641,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * Return the value of the {@code Cache-Control} header. */ public String getCacheControl() { - return getFirst(CACHE_CONTROL); + return getFieldValues(CACHE_CONTROL); } /** @@ -664,7 +662,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable * Return the value of the {@code Connection} header. */ public List<String> getConnection() { - return getFirstValueAsList(CONNECTION); + return getValuesAsList(CONNECTION); } /** @@ -783,6 +781,30 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** + * Set the (new) value of the {@code If-Match} header. + * @since 4.3 + */ + public void setIfMatch(String ifMatch) { + set(IF_MATCH, ifMatch); + } + + /** + * Set the (new) value of the {@code If-Match} header. + * @since 4.3 + */ + public void setIfMatch(List<String> ifMatchList) { + set(IF_MATCH, toCommaDelimitedString(ifMatchList)); + } + + /** + * Return the value of the {@code If-Match} header. + * @since 4.3 + */ + public List<String> getIfMatch() { + return getETagValuesAsList(IF_MATCH); + } + + /** * Set the (new) value of the {@code If-Modified-Since} header. * <p>The date should be specified as the number of milliseconds since * January 1, 1970 GMT. @@ -814,35 +836,31 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); } - protected String toCommaDelimitedString(List<String> list) { - StringBuilder builder = new StringBuilder(); - for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) { - String ifNoneMatch = iterator.next(); - builder.append(ifNoneMatch); - if (iterator.hasNext()) { - builder.append(", "); - } - } - return builder.toString(); - } - /** * Return the value of the {@code If-None-Match} header. */ public List<String> getIfNoneMatch() { - return getFirstValueAsList(IF_NONE_MATCH); + return getETagValuesAsList(IF_NONE_MATCH); } - protected List<String> getFirstValueAsList(String header) { - List<String> result = new ArrayList<String>(); - String value = getFirst(header); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(token); - } - } - return result; + /** + * Set the (new) value of the {@code If-Unmodified-Since} header. + * <p>The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + * @since 4.3 + */ + public void setIfUnmodifiedSince(long ifUnmodifiedSince) { + setDate(IF_UNMODIFIED_SINCE, ifUnmodifiedSince); + } + + /** + * Return the value of the {@code If-Unmodified-Since} header. + * <p>The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + * @since 4.3 + */ + public long getIfUnmodifiedSince() { + return getFirstDate(IF_UNMODIFIED_SINCE, false); } /** @@ -943,11 +961,43 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** + * Set the request header names (e.g. "Accept-Language") for which the + * response is subject to content negotiation and variances based on the + * value of those request headers. + * @param requestHeaders the request header names + * @since 4.3 + */ + public void setVary(List<String> requestHeaders) { + set(VARY, toCommaDelimitedString(requestHeaders)); + } + + /** + * Return the request header names subject to content negotiation. + * @since 4.3 + */ + public List<String> getVary() { + return getValuesAsList(VARY); + } + + /** + * Set the given date under the given header name after formatting it as a string + * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of + * {@link #set(String, String)} but for date headers. + * @since 3.2.4 + */ + public void setDate(String headerName, long date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); + dateFormat.setTimeZone(GMT); + set(headerName, dateFormat.format(new Date(date))); + } + + /** * Parse the first header value for the given header name as a date, * return -1 if there is no value, or raise {@link IllegalArgumentException} * if the value cannot be parsed as a date. * @param headerName the header name * @return the parsed date header, or -1 if none + * @since 3.2.4 */ public long getFirstDate(String headerName) { return getFirstDate(headerName, true); @@ -992,16 +1042,92 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable } /** - * Set the given date under the given header name after formatting it as a string - * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of - * {@link #set(String, String)} but for date headers. + * Return all values of a given header name, + * even if this header is set multiple times. + * @param headerName the header name + * @return all associated values + * @since 4.3 + */ + public List<String> getValuesAsList(String headerName) { + List<String> values = get(headerName); + if (values != null) { + List<String> result = new ArrayList<String>(); + for (String value : values) { + if (value != null) { + String[] tokens = StringUtils.tokenizeToStringArray(value, ","); + for (String token : tokens) { + result.add(token); + } + } + } + return result; + } + return Collections.emptyList(); + } + + /** + * Retrieve a combined result from the field values of the ETag header. + * @param headerName the header name + * @return the combined result + * @since 4.3 + */ + protected List<String> getETagValuesAsList(String headerName) { + List<String> values = get(headerName); + if (values != null) { + List<String> result = new ArrayList<String>(); + for (String value : values) { + if (value != null) { + Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); + while (matcher.find()) { + if ("*".equals(matcher.group())) { + result.add(matcher.group()); + } + else { + result.add(matcher.group(1)); + } + } + if (result.isEmpty()) { + throw new IllegalArgumentException( + "Could not parse header '" + headerName + "' with value '" + value + "'"); + } + } + } + return result; + } + return Collections.emptyList(); + } + + /** + * Retrieve a combined result from the field values of multi-valued headers. + * @param headerName the header name + * @return the combined result + * @since 4.3 */ - public void setDate(String headerName, long date) { - SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); - dateFormat.setTimeZone(GMT); - set(headerName, dateFormat.format(new Date(date))); + protected String getFieldValues(String headerName) { + List<String> headerValues = get(headerName); + return (headerValues != null ? toCommaDelimitedString(headerValues) : null); + } + + /** + * Turn the given list of header values into a comma-delimited result. + * @param headerValues the list of header values + * @return a combined result with comma delimitation + */ + protected String toCommaDelimitedString(List<String> headerValues) { + StringBuilder builder = new StringBuilder(); + for (Iterator<String> it = headerValues.iterator(); it.hasNext(); ) { + String val = it.next(); + builder.append(val); + if (it.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); } + + // MultiValueMap implementation + /** * Return the first header value for the given header name, if any. * @param headerName the header name diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index 87173fd3..c38c46fc 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -61,7 +61,7 @@ public enum HttpMethod { * @since 4.2.4 */ public boolean matches(String method) { - return name().equals(method); + return (this == resolve(method)); } } diff --git a/spring-web/src/main/java/org/springframework/http/HttpRange.java b/spring-web/src/main/java/org/springframework/http/HttpRange.java index 29f2e675..63d40e69 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpRange.java +++ b/spring-web/src/main/java/org/springframework/http/HttpRange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,16 @@ package org.springframework.http; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourceRegion; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -42,6 +46,30 @@ public abstract class HttpRange { /** + * Turn a {@code Resource} into a {@link ResourceRegion} using the range + * information contained in the current {@code HttpRange}. + * @param resource the {@code Resource} to select the region from + * @return the selected region of the given {@code Resource} + * @since 4.3 + */ + public ResourceRegion toResourceRegion(Resource resource) { + // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... + // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! + Assert.isTrue(InputStreamResource.class != resource.getClass(), + "Can't convert an InputStreamResource to a ResourceRegion"); + try { + long contentLength = resource.contentLength(); + Assert.isTrue(contentLength > 0, "Resource content length should be > 0"); + long start = getRangeStart(contentLength); + long end = getRangeEnd(contentLength); + return new ResourceRegion(resource, start, end - start + 1); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to convert Resource to ResourceRegion", ex); + } + } + + /** * Return the start of the range given the total length of a representation. * @param length the length of the representation * @return the start of this range for the representation @@ -134,6 +162,25 @@ public abstract class HttpRange { } /** + * Convert each {@code HttpRange} into a {@code ResourceRegion}, + * selecting the appropriate segment of the given {@code Resource} + * using the HTTP Range information. + * @param ranges the list of ranges + * @param resource the resource to select the regions from + * @return the list of regions for the given resource + */ + public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) { + if(ranges == null || ranges.size() == 0) { + return Collections.emptyList(); + } + List<ResourceRegion> regions = new ArrayList<ResourceRegion>(ranges.size()); + for(HttpRange range : ranges) { + regions.add(range.toResourceRegion(resource)); + } + return regions; + } + + /** * Return a string representation of the given list of {@code HttpRange} objects. * <p>This method can be used to for an {@code Range} header. * @param ranges the ranges to create a string of diff --git a/spring-web/src/main/java/org/springframework/http/HttpStatus.java b/spring-web/src/main/java/org/springframework/http/HttpStatus.java index d0f9b7c4..2c864624 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpStatus.java +++ b/spring-web/src/main/java/org/springframework/http/HttpStatus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,14 @@ package org.springframework.http; /** - * Java 5 enumeration of HTTP status codes. + * Enumeration of HTTP status codes. * * <p>The HTTP status code series can be retrieved via {@link #series()}. * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel + * @since 3.0 * @see HttpStatus.Series * @see <a href="http://www.iana.org/assignments/http-status-codes">HTTP Status Code Registry</a> * @see <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes">List of HTTP status codes - Wikipedia</a> @@ -321,6 +323,13 @@ public enum HttpStatus { * @see <a href="http://tools.ietf.org/html/rfc6585#section-5">Additional HTTP Status Codes</a> */ REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), + /** + * {@code 451 Unavailable For Legal Reasons}. + * @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-legally-restricted-status-04"> + * An HTTP Status Code to Report Legal Obstacles</a> + * @since 4.3 + */ + UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), // --- 5xx Server Error --- @@ -385,17 +394,17 @@ public enum HttpStatus { NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"); - private final int value; private final String reasonPhrase; - private HttpStatus(int value, String reasonPhrase) { + HttpStatus(int value, String reasonPhrase) { this.value = value; this.reasonPhrase = reasonPhrase; } + /** * Return the integer value of this status code. */ @@ -407,7 +416,7 @@ public enum HttpStatus { * Return the reason phrase of this status code. */ public String getReasonPhrase() { - return reasonPhrase; + return this.reasonPhrase; } /** @@ -416,7 +425,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is1xxInformational() { - return (Series.INFORMATIONAL.equals(series())); + return Series.INFORMATIONAL.equals(series()); } /** @@ -425,7 +434,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is2xxSuccessful() { - return (Series.SUCCESSFUL.equals(series())); + return Series.SUCCESSFUL.equals(series()); } /** @@ -434,7 +443,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is3xxRedirection() { - return (Series.REDIRECTION.equals(series())); + return Series.REDIRECTION.equals(series()); } @@ -444,7 +453,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is4xxClientError() { - return (Series.CLIENT_ERROR.equals(series())); + return Series.CLIENT_ERROR.equals(series()); } /** @@ -453,7 +462,7 @@ public enum HttpStatus { * This is a shortcut for checking the value of {@link #series()}. */ public boolean is5xxServerError() { - return (Series.SERVER_ERROR.equals(series())); + return Series.SERVER_ERROR.equals(series()); } /** @@ -469,7 +478,7 @@ public enum HttpStatus { */ @Override public String toString() { - return Integer.toString(value); + return Integer.toString(this.value); } @@ -490,10 +499,10 @@ public enum HttpStatus { /** - * Java 5 enumeration of HTTP status series. + * Enumeration of HTTP status series. * <p>Retrievable via {@link HttpStatus#series()}. */ - public static enum Series { + public enum Series { INFORMATIONAL(1), SUCCESSFUL(2), @@ -503,7 +512,7 @@ public enum HttpStatus { private final int value; - private Series(int value) { + Series(int value) { this.value = value; } @@ -527,7 +536,6 @@ public enum HttpStatus { public static Series valueOf(HttpStatus status) { return valueOf(status.value); } - } } diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 8b8e0485..924c1e4c 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -42,8 +43,7 @@ import org.springframework.util.comparator.CompoundComparator; * @author Rossen Stoyanchev * @author Sebastien Deleuze * @since 3.0 - * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics - * and Content, section 3.1.1.1</a> + * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.1">HTTP 1.1: Semantics and Content, section 3.1.1.1</a> */ public class MediaType extends MimeType implements Serializable { @@ -71,7 +71,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code application/x-www-form-urlencoded}. - * */ + */ public final static MediaType APPLICATION_FORM_URLENCODED; /** @@ -103,7 +103,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code application/octet-stream}. - * */ + */ public final static MediaType APPLICATION_OCTET_STREAM; /** @@ -112,8 +112,18 @@ public class MediaType extends MimeType implements Serializable { public final static String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; /** + * Public constant media type for {@code application/pdf}. + */ + public final static MediaType APPLICATION_PDF; + + /** + * A String equivalent of {@link MediaType#APPLICATION_PDF}. + */ + public final static String APPLICATION_PDF_VALUE = "application/pdf"; + + /** * Public constant media type for {@code application/xhtml+xml}. - * */ + */ public final static MediaType APPLICATION_XHTML_XML; /** @@ -163,7 +173,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code multipart/form-data}. - * */ + */ public final static MediaType MULTIPART_FORM_DATA; /** @@ -173,7 +183,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code text/html}. - * */ + */ public final static MediaType TEXT_HTML; /** @@ -182,8 +192,18 @@ public class MediaType extends MimeType implements Serializable { public final static String TEXT_HTML_VALUE = "text/html"; /** + * Public constant media type for {@code text/markdown}. + */ + public final static MediaType TEXT_MARKDOWN; + + /** + * A String equivalent of {@link MediaType#TEXT_MARKDOWN}. + */ + public final static String TEXT_MARKDOWN_VALUE = "text/markdown"; + + /** * Public constant media type for {@code text/plain}. - * */ + */ public final static MediaType TEXT_PLAIN; /** @@ -193,7 +213,7 @@ public class MediaType extends MimeType implements Serializable { /** * Public constant media type for {@code text/xml}. - * */ + */ public final static MediaType TEXT_XML; /** @@ -212,6 +232,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE); APPLICATION_JSON_UTF8 = valueOf(APPLICATION_JSON_UTF8_VALUE); APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE); + APPLICATION_PDF = valueOf(APPLICATION_PDF_VALUE); APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE); APPLICATION_XML = valueOf(APPLICATION_XML_VALUE); IMAGE_GIF = valueOf(IMAGE_GIF_VALUE); @@ -219,6 +240,7 @@ public class MediaType extends MimeType implements Serializable { IMAGE_PNG = valueOf(IMAGE_PNG_VALUE); MULTIPART_FORM_DATA = valueOf(MULTIPART_FORM_DATA_VALUE); TEXT_HTML = valueOf(TEXT_HTML_VALUE); + TEXT_MARKDOWN = valueOf(TEXT_MARKDOWN_VALUE); TEXT_PLAIN = valueOf(TEXT_PLAIN_VALUE); TEXT_XML = valueOf(TEXT_XML_VALUE); } @@ -268,6 +290,17 @@ public class MediaType extends MimeType implements Serializable { } /** + * Copy-constructor that copies the type, subtype and parameters of the given + * {@code MediaType}, and allows to set the specified character set. + * @param other the other media type + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contain illegal characters + */ + public MediaType(MediaType other, Charset charset) { + super(other, charset); + } + + /** * Copy-constructor that copies the type and subtype of the given {@code MediaType}, * and allows for different parameter. * @param other the other media type @@ -364,6 +397,8 @@ public class MediaType extends MimeType implements Serializable { * Parse the given String value into a {@code MediaType} object, * with this method name following the 'valueOf' naming convention * (as supported by {@link org.springframework.core.convert.ConversionService}. + * @param value the string to parse + * @throws InvalidMediaTypeException if the media type value cannot be parsed * @see #parseMediaType(String) */ public static MediaType valueOf(String value) { @@ -374,7 +409,7 @@ public class MediaType extends MimeType implements Serializable { * Parse the given String into a single {@code MediaType}. * @param mediaType the string to parse * @return the media type - * @throws InvalidMediaTypeException if the string cannot be parsed + * @throws InvalidMediaTypeException if the media type value cannot be parsed */ public static MediaType parseMediaType(String mediaType) { MimeType type; @@ -392,13 +427,12 @@ public class MediaType extends MimeType implements Serializable { } } - /** - * Parse the given, comma-separated string into a list of {@code MediaType} objects. + * Parse the given comma-separated string into a list of {@code MediaType} objects. * <p>This method can be used to parse an Accept or Content-Type header. * @param mediaTypes the string to parse * @return the list of media types - * @throws IllegalArgumentException if the string cannot be parsed + * @throws InvalidMediaTypeException if the media type value cannot be parsed */ public static List<MediaType> parseMediaTypes(String mediaTypes) { if (!StringUtils.hasLength(mediaTypes)) { @@ -413,6 +447,31 @@ public class MediaType extends MimeType implements Serializable { } /** + * Parse the given list of (potentially) comma-separated strings into a + * list of {@code MediaType} objects. + * <p>This method can be used to parse an Accept or Content-Type header. + * @param mediaTypes the string to parse + * @return the list of media types + * @throws InvalidMediaTypeException if the media type value cannot be parsed + * @since 4.3.2 + */ + public static List<MediaType> parseMediaTypes(List<String> mediaTypes) { + if (CollectionUtils.isEmpty(mediaTypes)) { + return Collections.<MediaType>emptyList(); + } + else if (mediaTypes.size() == 1) { + return parseMediaTypes(mediaTypes.get(0)); + } + else { + List<MediaType> result = new ArrayList<MediaType>(8); + for (String mediaType : mediaTypes) { + result.addAll(parseMediaTypes(mediaType)); + } + return result; + } + } + + /** * Return a string representation of the given list of {@code MediaType} objects. * <p>This method can be used to for an {@code Accept} or {@code Content-Type} header. * @param mediaTypes the media types to create a string representation for diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index 7e9ab88b..46dbba93 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.http; +import java.lang.reflect.Type; import java.net.URI; import java.nio.charset.Charset; import java.util.Arrays; @@ -54,6 +55,7 @@ import org.springframework.util.ObjectUtils; * </pre> * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 4.1 * @see #getMethod() * @see #getUrl() @@ -64,6 +66,8 @@ public class RequestEntity<T> extends HttpEntity<T> { private final URI url; + private final Type type; + /** * Constructor with method and URL but without body nor headers. @@ -81,7 +85,19 @@ public class RequestEntity<T> extends HttpEntity<T> { * @param url the URL */ public RequestEntity(T body, HttpMethod method, URI url) { - this(body, null, method, url); + this(body, null, method, url, null); + } + + /** + * Constructor with method, URL, body and type but without headers. + * @param body the body + * @param method the method + * @param url the URL + * @param type the type used for generic type resolution + * @since 4.3 + */ + public RequestEntity(T body, HttpMethod method, URI url, Type type) { + this(body, null, method, url, type); } /** @@ -91,7 +107,7 @@ public class RequestEntity<T> extends HttpEntity<T> { * @param url the URL */ public RequestEntity(MultiValueMap<String, String> headers, HttpMethod method, URI url) { - this(null, headers, method, url); + this(null, headers, method, url, null); } /** @@ -102,9 +118,23 @@ public class RequestEntity<T> extends HttpEntity<T> { * @param url the URL */ public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url) { + this(body, headers, method, url, null); + } + + /** + * Constructor with method, URL, headers, body and type. + * @param body the body + * @param headers the headers + * @param method the method + * @param url the URL + * @param type the type used for generic type resolution + * @since 4.3 + */ + public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url, Type type) { super(body, headers); this.method = method; this.url = url; + this.type = type; } @@ -124,6 +154,21 @@ public class RequestEntity<T> extends HttpEntity<T> { return this.url; } + /** + * Return the type of the request's body. + * @return the request's body type, or {@code null} if not known + * @since 4.3 + */ + public Type getType() { + if (this.type == null) { + T body = getBody(); + if (body != null) { + return body.getClass(); + } + } + return this.type; + } + @Override public boolean equals(Object other) { @@ -134,8 +179,8 @@ public class RequestEntity<T> extends HttpEntity<T> { return false; } RequestEntity<?> otherEntity = (RequestEntity<?>) other; - return (ObjectUtils.nullSafeEquals(this.method, otherEntity.method) && - ObjectUtils.nullSafeEquals(this.url, otherEntity.url)); + return (ObjectUtils.nullSafeEquals(getMethod(), otherEntity.getMethod()) && + ObjectUtils.nullSafeEquals(getUrl(), otherEntity.getUrl())); } @Override @@ -149,9 +194,9 @@ public class RequestEntity<T> extends HttpEntity<T> { @Override public String toString() { StringBuilder builder = new StringBuilder("<"); - builder.append(this.method); + builder.append(getMethod()); builder.append(' '); - builder.append(this.url); + builder.append(getUrl()); builder.append(','); T body = getBody(); HttpHeaders headers = getHeaders(); @@ -327,6 +372,16 @@ public class RequestEntity<T> extends HttpEntity<T> { * @return the built request entity */ <T> RequestEntity<T> body(T body); + + /** + * Set the body and type of the request entity and build the RequestEntity. + * @param <T> the type of the body + * @param body the body of the request entity + * @param type the type of the body, useful for generic type resolution + * @return the built request entity + * @since 4.3 + */ + <T> RequestEntity<T> body(T body, Type type); } @@ -396,6 +451,11 @@ public class RequestEntity<T> extends HttpEntity<T> { public <T> RequestEntity<T> body(T body) { return new RequestEntity<T>(body, this.headers, this.method, this.url); } + + @Override + public <T> RequestEntity<T> body(T body, Type type) { + return new RequestEntity<T>(body, this.headers, this.method, this.url, type); + } } } diff --git a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java index 9eba72c0..7edb9ea2 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; @@ -65,45 +66,55 @@ import org.springframework.util.ObjectUtils; */ public class ResponseEntity<T> extends HttpEntity<T> { - private final HttpStatus statusCode; + private final Object statusCode; /** * Create a new {@code ResponseEntity} with the given status code, and no body nor headers. - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(HttpStatus statusCode) { - super(); - this.statusCode = statusCode; + public ResponseEntity(HttpStatus status) { + this(null, null, status); } /** * Create a new {@code ResponseEntity} with the given body and status code, and no headers. * @param body the entity body - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(T body, HttpStatus statusCode) { - super(body); - this.statusCode = statusCode; + public ResponseEntity(T body, HttpStatus status) { + this(body, null, status); } /** * Create a new {@code HttpEntity} with the given headers and status code, and no body. * @param headers the entity headers - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus statusCode) { - super(headers); - this.statusCode = statusCode; + public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status) { + this(null, headers, status); } /** * Create a new {@code HttpEntity} with the given body, headers, and status code. * @param body the entity body * @param headers the entity headers - * @param statusCode the status code + * @param status the status code */ - public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus statusCode) { + public ResponseEntity(T body, MultiValueMap<String, String> headers, HttpStatus status) { + super(body, headers); + Assert.notNull(status, "HttpStatus must not be null"); + this.statusCode = status; + } + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * Just used behind the nested builder API. + * @param body the entity body + * @param headers the entity headers + * @param statusCode the status code (as {@code HttpStatus} or as {@code Integer} value) + */ + private ResponseEntity(T body, MultiValueMap<String, String> headers, Object statusCode) { super(body, headers); this.statusCode = statusCode; } @@ -111,10 +122,29 @@ public class ResponseEntity<T> extends HttpEntity<T> { /** * Return the HTTP status code of the response. - * @return the HTTP status as an HttpStatus enum value + * @return the HTTP status as an HttpStatus enum entry */ public HttpStatus getStatusCode() { - return this.statusCode; + if (this.statusCode instanceof HttpStatus) { + return (HttpStatus) this.statusCode; + } + else { + return HttpStatus.valueOf((Integer) this.statusCode); + } + } + + /** + * Return the HTTP status code of the response. + * @return the HTTP status as an int value + * @since 4.3 + */ + public int getStatusCodeValue() { + if (this.statusCode instanceof HttpStatus) { + return ((HttpStatus) this.statusCode).value(); + } + else { + return (Integer) this.statusCode; + } } @@ -139,8 +169,10 @@ public class ResponseEntity<T> extends HttpEntity<T> { public String toString() { StringBuilder builder = new StringBuilder("<"); builder.append(this.statusCode.toString()); - builder.append(' '); - builder.append(this.statusCode.getReasonPhrase()); + if (this.statusCode instanceof HttpStatus) { + builder.append(' '); + builder.append(((HttpStatus) this.statusCode).getReasonPhrase()); + } builder.append(','); T body = getBody(); HttpHeaders headers = getHeaders(); @@ -167,6 +199,7 @@ public class ResponseEntity<T> extends HttpEntity<T> { * @since 4.1 */ public static BodyBuilder status(HttpStatus status) { + Assert.notNull(status, "HttpStatus must not be null"); return new DefaultBuilder(status); } @@ -177,7 +210,7 @@ public class ResponseEntity<T> extends HttpEntity<T> { * @since 4.1 */ public static BodyBuilder status(int status) { - return status(HttpStatus.valueOf(status)); + return new DefaultBuilder(status); } /** @@ -333,6 +366,17 @@ public class ResponseEntity<T> extends HttpEntity<T> { B cacheControl(CacheControl cacheControl); /** + * Configure one or more request header names (e.g. "Accept-Language") to + * add to the "Vary" response header to inform clients that the response is + * subject to content negotiation and variances based on the value of the + * given request headers. The configured request header names are added only + * if not already present in the response "Vary" header. + * @param requestHeaders request header names + * @since 4.3 + */ + B varyBy(String... requestHeaders); + + /** * Build the response entity with no body. * @return the response entity * @see BodyBuilder#body(Object) @@ -377,12 +421,12 @@ public class ResponseEntity<T> extends HttpEntity<T> { private static class DefaultBuilder implements BodyBuilder { - private final HttpStatus status; + private final Object statusCode; private final HttpHeaders headers = new HttpHeaders(); - public DefaultBuilder(HttpStatus status) { - this.status = status; + public DefaultBuilder(Object statusCode) { + this.statusCode = statusCode; } @Override @@ -455,13 +499,19 @@ public class ResponseEntity<T> extends HttpEntity<T> { } @Override + public BodyBuilder varyBy(String... requestHeaders) { + this.headers.setVary(Arrays.asList(requestHeaders)); + return this; + } + + @Override public ResponseEntity<Void> build() { - return new ResponseEntity<Void>(null, this.headers, this.status); + return body(null); } @Override public <T> ResponseEntity<T> body(T body) { - return new ResponseEntity<T>(body, this.headers, this.status); + return new ResponseEntity<T>(body, this.headers, this.statusCode); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java new file mode 100644 index 00000000..2f09014a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestExecution.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * Represents the context of a client-side HTTP request execution. + * + * <p>Used to invoke the next interceptor in the interceptor chain, or - + * if the calling interceptor is last - execute the request itself. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 + * @see AsyncClientHttpRequestInterceptor + */ +public interface AsyncClientHttpRequestExecution { + + /** + * Resume the request execution by invoking the next interceptor in the chain + * or executing the request to the remote service. + * @param request the HTTP request, containing the HTTP method and headers + * @param body the body of the request + * @return a corresponding future handle + * @throws IOException in case of I/O errors + */ + ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body) throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java new file mode 100644 index 00000000..3fabe120 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestInterceptor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.support.InterceptingAsyncHttpAccessor; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * Intercepts client-side HTTP requests. Implementations of this interface can be + * {@linkplain org.springframework.web.client.AsyncRestTemplate#setInterceptors registered} + * with the {@link org.springframework.web.client.AsyncRestTemplate} as to modify + * the outgoing {@link HttpRequest} and/or register to modify the incoming + * {@link ClientHttpResponse} with help of a + * {@link org.springframework.util.concurrent.ListenableFutureAdapter}. + * + * <p>The main entry point for interceptors is {@link #intercept}. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 + * @see org.springframework.web.client.AsyncRestTemplate + * @see InterceptingAsyncHttpAccessor + */ +public interface AsyncClientHttpRequestInterceptor { + + /** + * Intercept the given request, and return a response future. The given + * {@link AsyncClientHttpRequestExecution} allows the interceptor to pass on + * the request to the next entity in the chain. + * <p>An implementation might follow this pattern: + * <ol> + * <li>Examine the {@linkplain HttpRequest request} and body</li> + * <li>Optionally {@linkplain org.springframework.http.client.support.HttpRequestWrapper + * wrap} the request to filter HTTP attributes.</li> + * <li>Optionally modify the body of the request.</li> + * <li>One of the following: + * <ul> + * <li>execute the request through {@link ClientHttpRequestExecution}</li> + * <li>don't execute the request to block the execution altogether</li> + * </ul> + * <li>Optionally adapt the response to filter HTTP attributes with the help of + * {@link org.springframework.util.concurrent.ListenableFutureAdapter + * ListenableFutureAdapter}.</li> + * </ol> + * @param request the request, containing method, URI, and headers + * @param body the body of the request + * @param execution the request execution + * @return the response future + * @throws IOException in case of I/O errors + */ + ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body, + AsyncClientHttpRequestExecution execution) throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java index df6e10c6..2fe64f52 100644 --- a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java +++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequestExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,22 +23,23 @@ import org.springframework.http.HttpRequest; /** * Represents the context of a client-side HTTP request execution. * - * <p>Used to invoke the next interceptor in the interceptor chain, or - if the calling interceptor is last - execute - * the request itself. + * <p>Used to invoke the next interceptor in the interceptor chain, + * or - if the calling interceptor is last - execute the request itself. * * @author Arjen Poutsma - * @see ClientHttpRequestInterceptor * @since 3.1 + * @see ClientHttpRequestInterceptor */ public interface ClientHttpRequestExecution { /** - * Execute the request with the given request attributes and body, and return the response. - * + * Execute the request with the given request attributes and body, + * and return the response. * @param request the request, containing method, URI, and headers * @param body the body of the request to execute * @return the response * @throws IOException in case of I/O errors */ ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException; + } diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index 53f6faf0..d61231d9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.apache.http.protocol.HttpContext; import org.springframework.beans.factory.DisposableBean; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that @@ -58,6 +59,20 @@ import org.springframework.util.Assert; */ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean { + private static Class<?> abstractHttpClientClass; + + static { + try { + // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3) + abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient", + HttpComponentsClientHttpRequestFactory.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Probably removed from HttpComponents in the meantime... + } + } + + private HttpClient httpClient; private RequestConfig requestConfig; @@ -130,7 +145,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest */ @SuppressWarnings("deprecation") private void setLegacyConnectionTimeout(HttpClient client, int timeout) { - if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { + if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) { client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); } } @@ -171,7 +186,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest */ @SuppressWarnings("deprecation") private void setLegacySocketTimeout(HttpClient client, int timeout) { - if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { + if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) { client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java new file mode 100644 index 00000000..0b13181a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; +import java.util.List; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.util.StreamUtils; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * An {@link AsyncClientHttpRequest} wrapper that enriches it proceeds the actual + * request execution with calling the registered interceptors. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @see InterceptingAsyncClientHttpRequestFactory + */ +class InterceptingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private AsyncClientHttpRequestFactory requestFactory; + + private List<AsyncClientHttpRequestInterceptor> interceptors; + + private URI uri; + + private HttpMethod httpMethod; + + + /** + * Creates new instance of {@link InterceptingAsyncClientHttpRequest}. + * + * @param requestFactory the async request factory + * @param interceptors the list of interceptors + * @param uri the request URI + * @param httpMethod the HTTP method + */ + public InterceptingAsyncClientHttpRequest(AsyncClientHttpRequestFactory requestFactory, + List<AsyncClientHttpRequestInterceptor> interceptors, URI uri, HttpMethod httpMethod) { + + this.requestFactory = requestFactory; + this.interceptors = interceptors; + this.uri = uri; + this.httpMethod = httpMethod; + } + + + @Override + protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] body) + throws IOException { + + return new AsyncRequestExecution().executeAsync(this, body); + } + + @Override + public HttpMethod getMethod() { + return httpMethod; + } + + @Override + public URI getURI() { + return uri; + } + + + private class AsyncRequestExecution implements AsyncClientHttpRequestExecution { + + private Iterator<AsyncClientHttpRequestInterceptor> iterator; + + public AsyncRequestExecution() { + this.iterator = interceptors.iterator(); + } + + @Override + public ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body) + throws IOException { + + if (this.iterator.hasNext()) { + AsyncClientHttpRequestInterceptor interceptor = this.iterator.next(); + return interceptor.intercept(request, body, this); + } + else { + URI theUri = request.getURI(); + HttpMethod theMethod = request.getMethod(); + HttpHeaders theHeaders = request.getHeaders(); + + AsyncClientHttpRequest delegate = requestFactory.createAsyncRequest(theUri, theMethod); + delegate.getHeaders().putAll(theHeaders); + if (body.length > 0) { + StreamUtils.copy(body, delegate.getBody()); + } + + return delegate.executeAsync(); + } + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java new file mode 100644 index 00000000..1ca68c52 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/InterceptingAsyncClientHttpRequestFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.net.URI; +import java.util.Collections; +import java.util.List; + +import org.springframework.http.HttpMethod; + +/** + * Wrapper for a {@link AsyncClientHttpRequestFactory} that has support for + * {@link AsyncClientHttpRequestInterceptor}s. + * + * @author Jakub Narloch + * @since 4.3 + * @see InterceptingAsyncClientHttpRequest + */ +public class InterceptingAsyncClientHttpRequestFactory implements AsyncClientHttpRequestFactory { + + private AsyncClientHttpRequestFactory delegate; + + private List<AsyncClientHttpRequestInterceptor> interceptors; + + + /** + * Create new instance of {@link InterceptingAsyncClientHttpRequestFactory} + * with delegated request factory and list of interceptors. + * @param delegate the request factory to delegate to + * @param interceptors the list of interceptors to use + */ + public InterceptingAsyncClientHttpRequestFactory(AsyncClientHttpRequestFactory delegate, + List<AsyncClientHttpRequestInterceptor> interceptors) { + + this.delegate = delegate; + this.interceptors = (interceptors != null ? interceptors : Collections.<AsyncClientHttpRequestInterceptor>emptyList()); + } + + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod method) { + return new InterceptingAsyncClientHttpRequest(this.delegate, this.interceptors, uri, method); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java index be619f1f..2baa53a4 100644 --- a/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/Netty4ClientHttpResponse.java @@ -56,11 +56,13 @@ class Netty4ClientHttpResponse extends AbstractClientHttpResponse { @Override + @SuppressWarnings("deprecation") public int getRawStatusCode() throws IOException { return this.nettyResponse.getStatus().code(); } @Override + @SuppressWarnings("deprecation") public String getStatusText() throws IOException { return this.nettyResponse.getStatus().reasonPhrase(); } diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java new file mode 100644 index 00000000..057a1652 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +/** + * {@link AsyncClientHttpRequest} implementation that uses OkHttp 3.x to execute requests. + * + * <p>Created via the {@link OkHttp3ClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +class OkHttp3AsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttp3AsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content) + throws IOException { + + Request request = OkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttpListenableFuture(this.client.newCall(request)); + } + + + private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> { + + private final Call call; + + public OkHttpListenableFuture(Call call) { + this.call = call; + this.call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + set(new OkHttp3ClientHttpResponse(response)); + } + @Override + public void onFailure(Call call, IOException ex) { + setException(ex); + } + }); + } + + @Override + protected void interruptTask() { + this.call.cancel(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java new file mode 100644 index 00000000..e0ebaac3 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * {@link ClientHttpRequest} implementation that uses OkHttp 3.x to execute requests. + * + * <p>Created via the {@link OkHttp3ClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +class OkHttp3ClientHttpRequest extends AbstractBufferingClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttp3ClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + + @Override + protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException { + Request request = OkHttp3ClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttp3ClientHttpResponse(this.client.newCall(request).execute()); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java new file mode 100644 index 00000000..546fde34 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactory.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link ClientHttpRequestFactory} implementation that uses + * <a href="http://square.github.io/okhttp/">OkHttp</a> 3.x to create requests. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +public class OkHttp3ClientHttpRequestFactory + implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean { + + private OkHttpClient client; + + private final boolean defaultClient; + + + /** + * Create a factory with a default {@link OkHttpClient} instance. + */ + public OkHttp3ClientHttpRequestFactory() { + this.client = new OkHttpClient(); + this.defaultClient = true; + } + + /** + * Create a factory with the given {@link OkHttpClient} instance. + * @param client the client to use + */ + public OkHttp3ClientHttpRequestFactory(OkHttpClient client) { + Assert.notNull(client, "OkHttpClient must not be null"); + this.client = client; + this.defaultClient = false; + } + + + /** + * Sets the underlying read timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * @see okhttp3.OkHttpClient.Builder#readTimeout(long, TimeUnit) + */ + public void setReadTimeout(int readTimeout) { + this.client = this.client.newBuilder() + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Sets the underlying write timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * @see okhttp3.OkHttpClient.Builder#writeTimeout(long, TimeUnit) + */ + public void setWriteTimeout(int writeTimeout) { + this.client = this.client.newBuilder() + .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Sets the underlying connect timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * @see okhttp3.OkHttpClient.Builder#connectTimeout(long, TimeUnit) + */ + public void setConnectTimeout(int connectTimeout) { + this.client = this.client.newBuilder() + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { + return new OkHttp3ClientHttpRequest(this.client, uri, httpMethod); + } + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) { + return new OkHttp3AsyncClientHttpRequest(this.client, uri, httpMethod); + } + + + @Override + public void destroy() throws IOException { + if (this.defaultClient) { + // Clean up the client if we created it in the constructor + if (this.client.cache() != null) { + this.client.cache().close(); + } + this.client.dispatcher().executorService().shutdown(); + } + } + + + static Request buildRequest(HttpHeaders headers, byte[] content, URI uri, + HttpMethod method) throws MalformedURLException { + + okhttp3.MediaType contentType = getContentType(headers); + RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null); + + URL url = uri.toURL(); + String methodName = method.name(); + Request.Builder builder = new Request.Builder().url(url).method(methodName, body); + + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + builder.addHeader(headerName, headerValue); + } + } + + return builder.build(); + } + + private static okhttp3.MediaType getContentType(HttpHeaders headers) { + String rawContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + return (StringUtils.hasText(rawContentType) ? okhttp3.MediaType.parse(rawContentType) : null); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java new file mode 100644 index 00000000..b6a7ed19 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttp3ClientHttpResponse.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.io.InputStream; + +import okhttp3.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +/** + * {@link ClientHttpResponse} implementation based on OkHttp 3.x. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +class OkHttp3ClientHttpResponse extends AbstractClientHttpResponse { + + private final Response response; + + private HttpHeaders headers; + + + public OkHttp3ClientHttpResponse(Response response) { + Assert.notNull(response, "Response must not be null"); + this.response = response; + } + + + @Override + public int getRawStatusCode() { + return this.response.code(); + } + + @Override + public String getStatusText() { + return this.response.message(); + } + + @Override + public InputStream getBody() throws IOException { + return this.response.body().byteStream(); + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + HttpHeaders headers = new HttpHeaders(); + for (String headerName : this.response.headers().names()) { + for (String headerValue : this.response.headers(headerName)) { + headers.add(headerName, headerValue); + } + } + this.headers = headers; + } + return this.headers; + } + + @Override + public void close() { + this.response.body().close(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java new file mode 100644 index 00000000..6b042422 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpAsyncClientHttpRequest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; + +import com.squareup.okhttp.Call; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +/** + * {@link AsyncClientHttpRequest} implementation that uses OkHttp 2.x to execute requests. + * + * <p>Created via the {@link OkHttpClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @since 4.3 + * @see org.springframework.http.client.OkHttp3AsyncClientHttpRequest + */ +class OkHttpAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttpAsyncClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content) + throws IOException { + + Request request = OkHttpClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttpListenableFuture(this.client.newCall(request)); + } + + + private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> { + + private final Call call; + + public OkHttpListenableFuture(Call call) { + this.call = call; + this.call.enqueue(new Callback() { + @Override + public void onResponse(Response response) { + set(new OkHttpClientHttpResponse(response)); + } + @Override + public void onFailure(Request request, IOException ex) { + setException(ex); + } + }); + } + + @Override + protected void interruptTask() { + this.call.cancel(); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java index fe519f06..a2f75be4 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,35 +18,24 @@ package org.springframework.http.client; import java.io.IOException; import java.net.URI; -import java.net.URL; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -import com.squareup.okhttp.Call; -import com.squareup.okhttp.Callback; -import com.squareup.okhttp.MediaType; + import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; -import com.squareup.okhttp.Response; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; /** - * {@link ClientHttpRequest} implementation that uses OkHttp to execute requests. + * {@link ClientHttpRequest} implementation that uses OkHttp 2.x to execute requests. * * <p>Created via the {@link OkHttpClientHttpRequestFactory}. * * @author Luciano Leggieri * @author Arjen Poutsma * @since 4.2 + * @see org.springframework.http.client.OkHttp3ClientHttpRequest */ -class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest implements ClientHttpRequest { +class OkHttpClientHttpRequest extends AbstractBufferingClientHttpRequest { private final OkHttpClient client; @@ -72,73 +61,11 @@ class OkHttpClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest im return this.uri; } - @Override - protected ListenableFuture<ClientHttpResponse> executeInternal(HttpHeaders headers, byte[] content) - throws IOException { - - MediaType contentType = getContentType(headers); - RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null); - - URL url = this.uri.toURL(); - String methodName = this.method.name(); - Request.Builder builder = new Request.Builder().url(url).method(methodName, body); - - for (Map.Entry<String, List<String>> entry : headers.entrySet()) { - String headerName = entry.getKey(); - for (String headerValue : entry.getValue()) { - builder.addHeader(headerName, headerValue); - } - } - Request request = builder.build(); - - return new OkHttpListenableFuture(this.client.newCall(request)); - } - - private MediaType getContentType(HttpHeaders headers) { - String rawContentType = headers.getFirst("Content-Type"); - return (StringUtils.hasText(rawContentType) ? MediaType.parse(rawContentType) : null); - } @Override - public ClientHttpResponse execute() throws IOException { - try { - return executeAsync().get(); - } - catch (InterruptedException ex) { - throw new IOException(ex.getMessage(), ex); - } - catch (ExecutionException ex) { - Throwable cause = ex.getCause(); - if (cause instanceof IOException) { - throw (IOException) cause; - } - throw new IOException(cause.getMessage(), cause); - } - } - - - private static class OkHttpListenableFuture extends SettableListenableFuture<ClientHttpResponse> { - - private final Call call; - - public OkHttpListenableFuture(Call call) { - this.call = call; - this.call.enqueue(new Callback() { - @Override - public void onResponse(Response response) { - set(new OkHttpClientHttpResponse(response)); - } - @Override - public void onFailure(Request request, IOException ex) { - setException(ex); - } - }); - } - - @Override - protected void interruptTask() { - this.call.cancel(); - } + protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException { + Request request = OkHttpClientHttpRequestFactory.buildRequest(headers, content, this.uri, this.method); + return new OkHttpClientHttpResponse(this.client.newCall(request).execute()); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java index 9b2674dd..d5ae9e97 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpRequestFactory.java @@ -16,22 +16,32 @@ package org.springframework.http.client; +import java.io.IOException; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link ClientHttpRequestFactory} implementation that uses - * <a href="http://square.github.io/okhttp/">OkHttp</a> to create requests. + * <a href="http://square.github.io/okhttp/">OkHttp</a> 2.x to create requests. * * @author Luciano Leggieri * @author Arjen Poutsma * @since 4.2 + * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory */ public class OkHttpClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory, DisposableBean { @@ -90,20 +100,17 @@ public class OkHttpClientHttpRequestFactory @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { - return createRequestInternal(uri, httpMethod); + return new OkHttpClientHttpRequest(this.client, uri, httpMethod); } @Override public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) { - return createRequestInternal(uri, httpMethod); + return new OkHttpAsyncClientHttpRequest(this.client, uri, httpMethod); } - private OkHttpClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { - return new OkHttpClientHttpRequest(this.client, uri, httpMethod); - } @Override - public void destroy() throws Exception { + public void destroy() throws IOException { if (this.defaultClient) { // Clean up the client if we created it in the constructor if (this.client.getCache() != null) { @@ -113,4 +120,31 @@ public class OkHttpClientHttpRequestFactory } } + + static Request buildRequest(HttpHeaders headers, byte[] content, URI uri, + HttpMethod method) throws MalformedURLException { + + com.squareup.okhttp.MediaType contentType = getContentType(headers); + RequestBody body = (content.length > 0 ? RequestBody.create(contentType, content) : null); + + URL url = uri.toURL(); + String methodName = method.name(); + Request.Builder builder = new Request.Builder().url(url).method(methodName, body); + + for (Map.Entry<String, List<String>> entry : headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + builder.addHeader(headerName, headerValue); + } + } + + return builder.build(); + } + + private static com.squareup.okhttp.MediaType getContentType(HttpHeaders headers) { + String rawContentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + return (StringUtils.hasText(rawContentType) ? + com.squareup.okhttp.MediaType.parse(rawContentType) : null); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java index 392a2d7d..6a0639f5 100644 --- a/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/OkHttpClientHttpResponse.java @@ -25,11 +25,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; /** - * {@link ClientHttpResponse} implementation based on OkHttp. + * {@link ClientHttpResponse} implementation based on OkHttp 2.x. * * @author Luciano Leggieri * @author Arjen Poutsma * @since 4.2 + * @see org.springframework.http.client.OkHttp3ClientHttpResponse */ class OkHttpClientHttpResponse extends AbstractClientHttpResponse { diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java index 015a2e42..bd439963 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,6 +89,10 @@ final class SimpleBufferingAsyncClientHttpRequest extends AbstractBufferingAsync if (connection.getDoOutput()) { FileCopyUtils.copy(bufferedOutput, connection.getOutputStream()); } + else { + // Immediately trigger the request in a no-output scenario as well + connection.getResponseCode(); + } return new SimpleClientHttpResponse(connection); } }); diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java index 24195beb..9a992f33 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,12 +68,10 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp @Override protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException { addHeaders(this.connection, headers); - // JDK <1.8 doesn't support getOutputStream with HTTP DELETE if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) { this.connection.setDoOutput(false); } - if (this.connection.getDoOutput() && this.outputStreaming) { this.connection.setFixedLengthStreamingMode(bufferedOutput.length); } @@ -81,7 +79,10 @@ final class SimpleBufferingClientHttpRequest extends AbstractBufferingClientHttp if (this.connection.getDoOutput()) { FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream()); } - + else { + // Immediately trigger the request in a no-output scenario as well + this.connection.getResponseCode(); + } return new SimpleClientHttpResponse(this.connection); } diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java index cc7e627d..f667cb2d 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.io.InputStream; import java.net.HttpURLConnection; import org.springframework.http.HttpHeaders; +import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** @@ -29,6 +30,7 @@ import org.springframework.util.StringUtils; * {@link SimpleStreamingClientHttpRequest#execute()}. * * @author Arjen Poutsma + * @author Brian Clozel * @since 3.0 */ final class SimpleClientHttpResponse extends AbstractClientHttpResponse { @@ -37,6 +39,8 @@ final class SimpleClientHttpResponse extends AbstractClientHttpResponse { private HttpHeaders headers; + private InputStream responseStream; + SimpleClientHttpResponse(HttpURLConnection connection) { this.connection = connection; @@ -78,12 +82,19 @@ final class SimpleClientHttpResponse extends AbstractClientHttpResponse { @Override public InputStream getBody() throws IOException { InputStream errorStream = this.connection.getErrorStream(); - return (errorStream != null ? errorStream : this.connection.getInputStream()); + this.responseStream = (errorStream != null ? errorStream : this.connection.getInputStream()); + return this.responseStream; } @Override public void close() { - this.connection.disconnect(); + if (this.responseStream != null) { + try { + StreamUtils.drain(this.responseStream); + this.responseStream.close(); + } + catch (IOException e) { } + } } } diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java index 0417eef0..e3327d23 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,6 +108,8 @@ final class SimpleStreamingAsyncClientHttpRequest extends AbstractAsyncClientHtt else { SimpleBufferingClientHttpRequest.addHeaders(connection, headers); connection.connect(); + // Immediately trigger the request in a no-output scenario as well + connection.getResponseCode(); } } catch (IOException ex) { diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java index 5e871d00..98ca2902 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,6 +94,8 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest { else { SimpleBufferingClientHttpRequest.addHeaders(this.connection, headers); this.connection.connect(); + // Immediately trigger the request in a no-output scenario as well + this.connection.getResponseCode(); } } catch (IOException ex) { diff --git a/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java new file mode 100644 index 00000000..ebf4a536 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthorizationInterceptor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client.support; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; + +/** + * {@link ClientHttpRequestInterceptor} to apply a BASIC authorization header. + * + * @author Phillip Webb + * @since 4.3.1 + */ +public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final String username; + + private final String password; + + + /** + * Create a new interceptor which adds a BASIC authorization header + * for the given username and password. + * @param username the username to use + * @param password the password to use + */ + public BasicAuthorizationInterceptor(String username, String password) { + Assert.hasLength(username, "Username must not be empty"); + this.username = username; + this.password = (password != null ? password : ""); + } + + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + + String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(UTF_8)); + request.getHeaders().add("Authorization", "Basic " + token); + return execution.execute(request, body); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java new file mode 100644 index 00000000..fd59f604 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingAsyncHttpAccessor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.http.client.AsyncClientHttpRequestFactory; +import org.springframework.http.client.AsyncClientHttpRequestInterceptor; +import org.springframework.http.client.InterceptingAsyncClientHttpRequestFactory; +import org.springframework.util.CollectionUtils; + +/** + * The HTTP accessor that extends the base {@link AsyncHttpAccessor} with + * request intercepting functionality. + * + * @author Jakub Narloch + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class InterceptingAsyncHttpAccessor extends AsyncHttpAccessor { + + private List<AsyncClientHttpRequestInterceptor> interceptors = + new ArrayList<AsyncClientHttpRequestInterceptor>(); + + + /** + * Set the request interceptors that this accessor should use. + * @param interceptors the list of interceptors + */ + public void setInterceptors(List<AsyncClientHttpRequestInterceptor> interceptors) { + this.interceptors = interceptors; + } + + /** + * Return the request interceptor that this accessor uses. + */ + public List<AsyncClientHttpRequestInterceptor> getInterceptors() { + return this.interceptors; + } + + + @Override + public AsyncClientHttpRequestFactory getAsyncRequestFactory() { + AsyncClientHttpRequestFactory delegate = super.getAsyncRequestFactory(); + if (!CollectionUtils.isEmpty(getInterceptors())) { + return new InterceptingAsyncClientHttpRequestFactory(delegate, getInterceptors()); + } + else { + return delegate; + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index d63d2870..e990fcd7 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -18,6 +18,7 @@ package org.springframework.http.converter; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -42,6 +43,7 @@ import org.springframework.util.Assert; * * @author Arjen Poutsma * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 3.0 */ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> { @@ -51,6 +53,8 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv private List<MediaType> supportedMediaTypes = Collections.emptyList(); + private Charset defaultCharset; + /** * Construct an {@code AbstractHttpMessageConverter} with no supported media types. @@ -75,6 +79,18 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); } + /** + * Construct an {@code AbstractHttpMessageConverter} with a default charset and + * multiple supported media types. + * @param defaultCharset the default character set + * @param supportedMediaTypes the supported media types + * @since 4.3 + */ + protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) { + this.defaultCharset = defaultCharset; + setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); + } + /** * Set the list of {@link MediaType} objects supported by this converter. @@ -89,6 +105,22 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv return Collections.unmodifiableList(this.supportedMediaTypes); } + /** + * Set the default character set, if any. + * @since 4.3 + */ + public void setDefaultCharset(Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Return the default character set, if any. + * @since 4.3 + */ + public Charset getDefaultCharset() { + return this.defaultCharset; + } + /** * This implementation checks if the given class is {@linkplain #supports(Class) supported}, @@ -200,7 +232,8 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv /** * Add default headers to the output message. * <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content - * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers. + * type was not provided, set if necessary the default character set, calls + * {@link #getContentLength}, and sets the corresponding headers. * @since 4.2 */ protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{ @@ -214,6 +247,12 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); } if (contentTypeToUse != null) { + if (contentTypeToUse.getCharset() == null) { + Charset defaultCharset = getDefaultCharset(); + if (defaultCharset != null) { + contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset); + } + } headers.setContentType(contentTypeToUse); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index fa2d0e4a..643a8713 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -82,6 +82,7 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.0 * @see MultiValueMap */ @@ -90,14 +91,14 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); - private Charset charset = DEFAULT_CHARSET; - - private Charset multipartCharset; - private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>(); private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>(); + private Charset charset = DEFAULT_CHARSET; + + private Charset multipartCharset; + public FormHttpMessageConverter() { this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); @@ -108,30 +109,10 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue stringHttpMessageConverter.setWriteAcceptCharset(false); this.partConverters.add(stringHttpMessageConverter); this.partConverters.add(new ResourceHttpMessageConverter()); - } - - /** - * Set the default character set to use for reading and writing form data when - * the request or response Content-Type header does not explicitly specify it. - * <p>By default this is set to "UTF-8". - */ - public void setCharset(Charset charset) { - this.charset = charset; + applyDefaultCharset(); } - /** - * Set the character set to use when writing multipart data to encode file - * names. Encoding is based on the encoded-word syntax defined in RFC 2047 - * and relies on {@code MimeUtility} from "javax.mail". - * <p>If not set file names will be encoded as US-ASCII. - * @param multipartCharset the charset to use - * @since 4.1.1 - * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a> - */ - public void setMultipartCharset(Charset multipartCharset) { - this.multipartCharset = multipartCharset; - } /** * Set the list of {@link MediaType} objects supported by this converter. @@ -163,6 +144,49 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue this.partConverters.add(partConverter); } + /** + * Set the default character set to use for reading and writing form data when + * the request or response Content-Type header does not explicitly specify it. + * <p>By default this is set to "UTF-8". As of 4.3, it will also be used as + * the default charset for the conversion of text bodies in a multipart request. + * In contrast to this, {@link #setMultipartCharset} only affects the encoding of + * <i>file names</i> in a multipart request according to the encoded-word syntax. + */ + public void setCharset(Charset charset) { + if (charset != this.charset) { + this.charset = (charset != null ? charset : DEFAULT_CHARSET); + applyDefaultCharset(); + } + } + + /** + * Apply the configured charset as a default to registered part converters. + */ + private void applyDefaultCharset() { + for (HttpMessageConverter<?> candidate : this.partConverters) { + if (candidate instanceof AbstractHttpMessageConverter) { + AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate; + // Only override default charset if the converter operates with a charset to begin with... + if (converter.getDefaultCharset() != null) { + converter.setDefaultCharset(this.charset); + } + } + } + } + + /** + * Set the character set to use when writing multipart data to encode file + * names. Encoding is based on the encoded-word syntax defined in RFC 2047 + * and relies on {@code MimeUtility} from "javax.mail". + * <p>If not set file names will be encoded as US-ASCII. + * @param multipartCharset the charset to use + * @since 4.1.1 + * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a> + */ + public void setMultipartCharset(Charset multipartCharset) { + this.multipartCharset = multipartCharset; + } + @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { @@ -202,7 +226,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { MediaType contentType = inputMessage.getHeaders().getContentType(); - Charset charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset); + Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset); String body = StreamUtils.copyToString(inputMessage.getBody(), charset); String[] pairs = StringUtils.tokenizeToStringArray(body, "&"); @@ -255,7 +279,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue Charset charset; if (contentType != null) { outputMessage.getHeaders().setContentType(contentType); - charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset); + charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset); } else { outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); diff --git a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java index 5fab4fec..2e01e5eb 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ObjectToStringHttpMessageConverter.java @@ -71,7 +71,7 @@ public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConve * @param defaultCharset the default charset */ public ObjectToStringHttpMessageConverter(ConversionService conversionService, Charset defaultCharset) { - super(new MediaType("text", "plain", defaultCharset)); + super(defaultCharset, MediaType.TEXT_PLAIN); Assert.notNull(conversionService, "ConversionService is required"); this.conversionService = conversionService; diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index cc8da360..93bd3a3a 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.http.converter; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import javax.activation.FileTypeMap; @@ -33,12 +34,14 @@ import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; /** - * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}. + * Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources} + * and supports byte range requests. * * <p>By default, this converter can read all media types. The Java Activation Framework (JAF) - * if available - is used to determine the {@code Content-Type} of written resources. * If JAF is not available, {@code application/octet-stream} is used. * + * * @author Arjen Poutsma * @author Juergen Hoeller * @author Kazuki Shimizu @@ -64,7 +67,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - if (InputStreamResource.class == clazz){ + if (InputStreamResource.class == clazz) { return new InputStreamResource(inputMessage.getBody()); } else if (clazz.isAssignableFrom(ByteArrayResource.class)) { @@ -90,25 +93,42 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R protected Long getContentLength(Resource resource, MediaType contentType) throws IOException { // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! - return (InputStreamResource.class == resource.getClass() ? null : resource.contentLength()); + if (InputStreamResource.class == resource.getClass()) { + return null; + } + long contentLength = resource.contentLength(); + return (contentLength < 0 ? null : contentLength); } @Override protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { - InputStream in = resource.getInputStream(); + writeContent(resource, outputMessage); + } + + protected void writeContent(Resource resource, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { try { - StreamUtils.copy(in, outputMessage.getBody()); - } - finally { + InputStream in = resource.getInputStream(); try { - in.close(); + StreamUtils.copy(in, outputMessage.getBody()); + } + catch (NullPointerException ex) { + // ignore, see SPR-13620 } - catch (IOException ex) { + finally { + try { + in.close(); + } + catch (Throwable ex) { + // ignore, see SPR-12999 + } } } - outputMessage.getBody().flush(); + catch (FileNotFoundException ex) { + // ignore, see SPR-12999 + } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java new file mode 100644 index 00000000..5c12092e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +import org.springframework.core.io.support.ResourceRegion; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StreamUtils; + +/** + * Implementation of {@link HttpMessageConverter} that can write a single {@link ResourceRegion}, + * or Collections of {@link ResourceRegion ResourceRegions}. + * + * @author Brian Clozel + * @since 4.3 + */ +public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { + + public ResourceRegionHttpMessageConverter() { + super(MediaType.ALL); + } + + + @Override + protected boolean supports(Class<?> clazz) { + // should not be called as we override canRead/canWrite + return false; + } + + @Override + public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) { + return false; + } + + @Override + public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + return null; + } + + @Override + protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + return null; + } + + @Override + public boolean canWrite(Class<?> clazz, MediaType mediaType) { + return canWrite(clazz, null, mediaType); + } + + @Override + public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) { + if (!(type instanceof ParameterizedType)) { + return ResourceRegion.class.isAssignableFrom((Class) type); + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!(parameterizedType.getRawType() instanceof Class)) { + return false; + } + Class<?> rawType = (Class<?>) parameterizedType.getRawType(); + if (!(Collection.class.isAssignableFrom(rawType))) { + return false; + } + if (parameterizedType.getActualTypeArguments().length != 1) { + return false; + } + Type typeArgument = parameterizedType.getActualTypeArguments()[0]; + if (!(typeArgument instanceof Class)) { + return false; + } + Class<?> typeArgumentClass = (Class<?>) typeArgument; + return typeArgumentClass.isAssignableFrom(ResourceRegion.class); + } + + @Override + @SuppressWarnings("unchecked") + protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + if (object instanceof ResourceRegion) { + writeResourceRegion((ResourceRegion) object, outputMessage); + } + else { + Collection<ResourceRegion> regions = (Collection<ResourceRegion>) object; + if(regions.size() == 1) { + writeResourceRegion(regions.iterator().next(), outputMessage); + } + else { + writeResourceRegionCollection((Collection<ResourceRegion>) object, outputMessage); + } + } + } + + protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException { + Assert.notNull(region, "ResourceRegion must not be null"); + HttpHeaders responseHeaders = outputMessage.getHeaders(); + long start = region.getPosition(); + long end = start + region.getCount() - 1; + Long resourceLength = region.getResource().contentLength(); + end = Math.min(end, resourceLength - 1); + long rangeLength = end - start + 1; + responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + resourceLength); + responseHeaders.setContentLength(rangeLength); + InputStream in = region.getResource().getInputStream(); + try { + StreamUtils.copyRange(in, outputMessage.getBody(), start, end); + } + finally { + try { + in.close(); + } + catch (IOException ex) { + // ignore + } + } + } + + private void writeResourceRegionCollection(Collection<ResourceRegion> resourceRegions, + HttpOutputMessage outputMessage) throws IOException { + + Assert.notNull(resourceRegions, "Collection of ResourceRegion should not be null"); + HttpHeaders responseHeaders = outputMessage.getHeaders(); + MediaType contentType = responseHeaders.getContentType(); + String boundaryString = MimeTypeUtils.generateMultipartBoundaryString(); + responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString); + OutputStream out = outputMessage.getBody(); + for (ResourceRegion region : resourceRegions) { + long start = region.getPosition(); + long end = start + region.getCount() - 1; + InputStream in = region.getResource().getInputStream(); + // Writing MIME header. + println(out); + print(out, "--" + boundaryString); + println(out); + if (contentType != null) { + print(out, "Content-Type: " + contentType.toString()); + println(out); + } + Long resourceLength = region.getResource().contentLength(); + end = Math.min(end, resourceLength - 1); + print(out, "Content-Range: bytes " + start + "-" + end + "/" + resourceLength); + println(out); + println(out); + // Printing content + StreamUtils.copyRange(in, out, start, end); + } + println(out); + print(out, "--" + boundaryString + "--"); + } + + + + private static void println(OutputStream os) throws IOException { + os.write('\r'); + os.write('\n'); + } + + private static void print(OutputStream os, String buf) throws IOException { + os.write(buf.getBytes("US-ASCII")); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 15c2693c..dc150054 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.util.StreamUtils; * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 3.0 */ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> { @@ -42,8 +43,6 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); - private final Charset defaultCharset; - private final List<Charset> availableCharsets; private boolean writeAcceptCharset = true; @@ -62,8 +61,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str * type does not specify one. */ public StringHttpMessageConverter(Charset defaultCharset) { - super(new MediaType("text", "plain", defaultCharset), MediaType.ALL); - this.defaultCharset = defaultCharset; + super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL); this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values()); } @@ -121,11 +119,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str } private Charset getContentTypeCharset(MediaType contentType) { - if (contentType != null && contentType.getCharSet() != null) { - return contentType.getCharSet(); + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); } else { - return this.defaultCharset; + return getDefaultCharset(); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java index 834fc820..9daa030f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/feed/AbstractWireFeedHttpMessageConverter.java @@ -66,7 +66,7 @@ public abstract class AbstractWireFeedHttpMessageConverter<T extends WireFeed> e WireFeedInput feedInput = new WireFeedInput(); MediaType contentType = inputMessage.getHeaders().getContentType(); Charset charset = - (contentType != null && contentType.getCharSet() != null? contentType.getCharSet() : DEFAULT_CHARSET); + (contentType != null && contentType.getCharset() != null? contentType.getCharset() : DEFAULT_CHARSET); try { Reader reader = new InputStreamReader(inputMessage.getBody(), charset); return (T) feedInput.build(reader); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index f29dc806..2d3dd80e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -17,22 +17,25 @@ package org.springframework.http.converter.json; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.nio.charset.Charset; import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.ser.FilterProvider; import com.fasterxml.jackson.databind.type.TypeFactory; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -41,14 +44,13 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.TypeUtils; /** * Abstract base class for Jackson based and content type independent * {@link HttpMessageConverter} implementations. * - * <p>Compatible with Jackson 2.1 to 2.6. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Arjen Poutsma * @author Keith Donald @@ -61,14 +63,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); - // Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference - private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class, - "canDeserialize", JavaType.class, AtomicReference.class); - - // Check for Jackson 2.6+ for support of generic type aware serialization of polymorphic collections - private static final boolean jackson26Available = ClassUtils.hasMethod(ObjectMapper.class, - "setDefaultPrettyPrinter", PrettyPrinter.class); - protected ObjectMapper objectMapper; @@ -77,16 +71,19 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; + setDefaultCharset(DEFAULT_CHARSET); } protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) { super(supportedMediaType); this.objectMapper = objectMapper; + setDefaultCharset(DEFAULT_CHARSET); } protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) { super(supportedMediaTypes); this.objectMapper = objectMapper; + setDefaultCharset(DEFAULT_CHARSET); } @@ -146,23 +143,14 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return false; } JavaType javaType = getJavaType(type, contextClass); - if (!jackson23Available || !logger.isWarnEnabled()) { + if (!logger.isWarnEnabled()) { return this.objectMapper.canDeserialize(javaType); } AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); if (this.objectMapper.canDeserialize(javaType, causeRef)) { return true; } - Throwable cause = causeRef.get(); - if (cause != null) { - String msg = "Failed to evaluate Jackson deserialization for type " + javaType; - if (logger.isDebugEnabled()) { - logger.warn(msg, cause); - } - else { - logger.warn(msg + ": " + cause); - } - } + logWarningIfNecessary(javaType, causeRef.get()); return false; } @@ -171,16 +159,29 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener if (!canWrite(mediaType)) { return false; } - if (!jackson23Available || !logger.isWarnEnabled()) { + if (!logger.isWarnEnabled()) { return this.objectMapper.canSerialize(clazz); } AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); if (this.objectMapper.canSerialize(clazz, causeRef)) { return true; } - Throwable cause = causeRef.get(); - if (cause != null) { - String msg = "Failed to evaluate Jackson serialization for type [" + clazz + "]"; + logWarningIfNecessary(clazz, causeRef.get()); + return false; + } + + /** + * Determine whether to log the given exception coming from a + * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check. + * @param type the class that Jackson tested for (de-)serializability + * @param cause the Jackson-thrown exception to evaluate + * (typically a {@link JsonMappingException}) + * @since 4.3 + */ + protected void logWarningIfNecessary(Type type, Throwable cause) { + if (cause != null && !(cause instanceof JsonMappingException && cause.getMessage().startsWith("Can not find"))) { + String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + + "serialization for type [" + type + "]"; if (logger.isDebugEnabled()) { logger.warn(msg, cause); } @@ -188,7 +189,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener logger.warn(msg + ": " + cause); } } - return false; } @Override @@ -213,13 +213,12 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener return readJavaType(javaType, inputMessage); } - @SuppressWarnings("deprecation") private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { try { if (inputMessage instanceof MappingJacksonInputMessage) { Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView(); if (deserializationView != null) { - return this.objectMapper.readerWithView(deserializationView).withType(javaType). + return this.objectMapper.readerWithView(deserializationView).forType(javaType). readValue(inputMessage.getBody()); } } @@ -231,7 +230,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } @Override - @SuppressWarnings("deprecation") protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -250,7 +248,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener serializationView = container.getSerializationView(); filters = container.getFilters(); } - if (jackson26Available && type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { + if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { javaType = getJavaType(type, null); } ObjectWriter objectWriter; @@ -264,7 +262,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener objectWriter = this.objectMapper.writer(); } if (javaType != null && javaType.isContainerType()) { - objectWriter = objectWriter.withType(javaType); + objectWriter = objectWriter.forType(javaType); } objectWriter.writeValue(generator, value); @@ -313,10 +311,62 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener * @return the Jackson JavaType */ protected JavaType getJavaType(Type type, Class<?> contextClass) { - TypeFactory tf = this.objectMapper.getTypeFactory(); - // Conditional call because Jackson 2.7 does not support null contextClass anymore - // TypeVariable resolution will not work with Jackson 2.7, see SPR-13853 for more details - return (contextClass != null ? tf.constructType(type, contextClass) : tf.constructType(type)); + TypeFactory typeFactory = this.objectMapper.getTypeFactory(); + if (contextClass != null) { + ResolvableType resolvedType = ResolvableType.forType(type); + if (type instanceof TypeVariable) { + ResolvableType resolvedTypeVariable = resolveVariable( + (TypeVariable<?>) type, ResolvableType.forClass(contextClass)); + if (resolvedTypeVariable != ResolvableType.NONE) { + return typeFactory.constructType(resolvedTypeVariable.resolve()); + } + } + else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length]; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + if (typeArgument instanceof TypeVariable) { + ResolvableType resolvedTypeArgument = resolveVariable( + (TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass)); + if (resolvedTypeArgument != ResolvableType.NONE) { + generics[i] = resolvedTypeArgument.resolve(); + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + return typeFactory.constructType(ResolvableType. + forClassWithGenerics(resolvedType.getRawClass(), generics).getType()); + } + } + return typeFactory.constructType(type); + } + + private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) { + ResolvableType resolvedType; + if (contextType.hasGenerics()) { + resolvedType = ResolvableType.forType(typeVariable, contextType); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + resolvedType = resolveVariable(typeVariable, contextType.getSuperType()); + if (resolvedType.resolve() != null) { + return resolvedType; + } + for (ResolvableType ifc : contextType.getInterfaces()) { + resolvedType = resolveVariable(typeVariable, ifc); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + return ResolvableType.NONE; } /** @@ -325,8 +375,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener * @return the JSON encoding to use (never {@code null}) */ protected JsonEncoding getJsonEncoding(MediaType contentType) { - if (contentType != null && contentType.getCharSet() != null) { - Charset charset = contentType.getCharSet(); + if (contentType != null && contentType.getCharset() != null) { + Charset charset = contentType.getCharset(); for (JsonEncoding encoding : JsonEncoding.values()) { if (charset.name().equals(encoding.getJavaName())) { return encoding; diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index 38f7f432..d82bd28d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -68,7 +68,8 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte * Construct a new {@code GsonHttpMessageConverter}. */ public GsonHttpMessageConverter() { - super(MediaType.APPLICATION_JSON_UTF8, new MediaType("application", "*+json", DEFAULT_CHARSET)); + super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + this.setDefaultCharset(DEFAULT_CHARSET); } @@ -177,10 +178,10 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte } private Charset getCharset(HttpHeaders headers) { - if (headers == null || headers.getContentType() == null || headers.getContentType().getCharSet() == null) { + if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) { return DEFAULT_CHARSET; } - return headers.getContentType().getCharSet(); + return headers.getContentType().getCharset(); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index b2ee1e5b..046a3b1e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,8 @@ import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; +import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import org.springframework.beans.BeanUtils; @@ -73,9 +75,10 @@ import org.springframework.util.StringUtils; * <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>: support for other Java 8 types like {@link java.util.Optional}</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>: support for Java 8 Date & Time API types</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-joda">jackson-datatype-joda</a>: support for Joda-Time types</li> + * <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>: support for Kotlin classes and data classes</li> * </ul> * - * <p>Tested against Jackson 2.4, 2.5, 2.6; compatible with Jackson 2.0 and higher. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Sebastien Deleuze * @author Juergen Hoeller @@ -127,6 +130,8 @@ public class Jackson2ObjectMapperBuilder { private ApplicationContext applicationContext; + private Boolean defaultUseWrapper; + /** * If set to {@code true}, an {@link XmlMapper} will be created using its @@ -279,8 +284,7 @@ public class Jackson2ObjectMapperBuilder { /** * Configure custom serializers. Each serializer is registered for the type - * returned by {@link JsonSerializer#handledType()}, which must not be - * {@code null}. + * returned by {@link JsonSerializer#handledType()}, which must not be {@code null}. * @see #serializersByType(Map) */ public Jackson2ObjectMapperBuilder serializers(JsonSerializer<?>... serializers) { @@ -320,6 +324,25 @@ public class Jackson2ObjectMapperBuilder { } /** + * Configure custom deserializers. Each deserializer is registered for the type + * returned by {@link JsonDeserializer#handledType()}, which must not be {@code null}. + * @since 4.3 + * @see #deserializersByType(Map) + */ + public Jackson2ObjectMapperBuilder deserializers(JsonDeserializer<?>... deserializers) { + if (deserializers != null) { + for (JsonDeserializer<?> deserializer : deserializers) { + Class<?> handledType = deserializer.handledType(); + if (handledType == null || handledType == Object.class) { + throw new IllegalArgumentException("Unknown handled type in " + deserializer.getClass().getName()); + } + this.deserializers.put(deserializer.handledType(), deserializer); + } + } + return this; + } + + /** * Configure a custom deserializer for the given type. * @since 4.1.2 */ @@ -393,6 +416,16 @@ public class Jackson2ObjectMapperBuilder { } /** + * Define if a wrapper will be used for indexed (List, array) properties or not by + * default (only applies to {@link XmlMapper}). + * @since 4.3 + */ + public Jackson2ObjectMapperBuilder defaultUseWrapper(boolean defaultUseWrapper) { + this.defaultUseWrapper = defaultUseWrapper; + return this; + } + + /** * Specify features to enable. * @see com.fasterxml.jackson.core.JsonParser.Feature * @see com.fasterxml.jackson.core.JsonGenerator.Feature @@ -547,7 +580,9 @@ public class Jackson2ObjectMapperBuilder { public <T extends ObjectMapper> T build() { ObjectMapper mapper; if (this.createXmlMapper) { - mapper = new XmlObjectMapperInitializer().create(); + mapper = (this.defaultUseWrapper != null ? + new XmlObjectMapperInitializer().create(this.defaultUseWrapper) : + new XmlObjectMapperInitializer().create()); } else { mapper = new ObjectMapper(); @@ -561,7 +596,6 @@ public class Jackson2ObjectMapperBuilder { * settings. This can be applied to any number of {@code ObjectMappers}. * @param objectMapper the ObjectMapper to configure */ - @SuppressWarnings("deprecation") public void configure(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); @@ -609,13 +643,11 @@ public class Jackson2ObjectMapperBuilder { } if (this.filters != null) { - // Deprecated as of Jackson 2.6, but just in favor of a fluent variant. - objectMapper.setFilters(this.filters); + objectMapper.setFilterProvider(this.filters); } for (Class<?> target : this.mixIns.keySet()) { - // Deprecated as of Jackson 2.5, but just in favor of a fluent variant. - objectMapper.addMixInAnnotations(target, this.mixIns.get(target)); + objectMapper.addMixIn(target, this.mixIns.get(target)); } if (!this.serializers.isEmpty() || !this.deserializers.isEmpty()) { @@ -693,7 +725,7 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> jdk7Module = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk7.Jdk7Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jdk7Module)); + objectMapper.registerModule(BeanUtils.instantiateClass(jdk7Module)); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk7 not available @@ -705,7 +737,7 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> jdk8Module = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jdk8Module)); + objectMapper.registerModule(BeanUtils.instantiateClass(jdk8Module)); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk8 not available @@ -717,18 +749,10 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> javaTimeModule = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(javaTimeModule)); + objectMapper.registerModule(BeanUtils.instantiateClass(javaTimeModule)); } catch (ClassNotFoundException ex) { - // jackson-datatype-jsr310 not available or older than 2.6 - try { - Class<? extends Module> jsr310Module = (Class<? extends Module>) - ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JSR310Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jsr310Module)); - } - catch (ClassNotFoundException ex2) { - // OK, jackson-datatype-jsr310 not available at all... - } + // jackson-datatype-jsr310 not available } } @@ -737,12 +761,24 @@ public class Jackson2ObjectMapperBuilder { try { Class<? extends Module> jodaModule = (Class<? extends Module>) ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiate(jodaModule)); + objectMapper.registerModule(BeanUtils.instantiateClass(jodaModule)); } catch (ClassNotFoundException ex) { // jackson-datatype-joda not available } } + + // Kotlin present? + if (ClassUtils.isPresent("kotlin.Unit", this.moduleClassLoader)) { + try { + Class<? extends Module> kotlinModule = (Class<? extends Module>) + ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader); + objectMapper.registerModule(BeanUtils.instantiateClass(kotlinModule)); + } + catch (ClassNotFoundException ex) { + // jackson-module-kotlin not available + } + } } @@ -768,11 +804,21 @@ public class Jackson2ObjectMapperBuilder { private static class XmlObjectMapperInitializer { public ObjectMapper create() { + return new XmlMapper(xmlInputFactory()); + } + + public ObjectMapper create(boolean defaultUseWrapper) { + JacksonXmlModule module = new JacksonXmlModule(); + module.setDefaultUseWrapper(defaultUseWrapper); + return new XmlMapper(new XmlFactory(xmlInputFactory()), module); + } + + private static XMLInputFactory xmlInputFactory() { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); inputFactory.setXMLResolver(NO_OP_XML_RESOLVER); - return new XmlMapper(inputFactory); + return inputFactory; } private static final XMLResolver NO_OP_XML_RESOLVER = new XMLResolver() { diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java index f17016e9..d945a7fb 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,6 +116,7 @@ import org.springframework.context.ApplicationContextAware; * <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>: support for other Java 8 types like {@link java.util.Optional}</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>: support for Java 8 Date & Time API types</li> * <li><a href="https://github.com/FasterXML/jackson-datatype-joda">jackson-datatype-joda</a>: support for Joda-Time types</li> + * <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>: support for Kotlin classes and data classes</li> * </ul> * * <p>In case you want to configure Jackson's {@link ObjectMapper} with a custom {@link Module}, @@ -127,7 +128,7 @@ import org.springframework.context.ApplicationContextAware; * </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) { diff --git a/spring-web/src/test/java/org/springframework/http/CacheControlTests.java b/spring-web/src/test/java/org/springframework/http/CacheControlTests.java index 43de2c0b..2111d2f1 100644 --- a/spring-web/src/test/java/org/springframework/http/CacheControlTests.java +++ b/spring-web/src/test/java/org/springframework/http/CacheControlTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,4 +63,17 @@ public class CacheControlTests { CacheControl cc = CacheControl.noStore(); assertThat(cc.getHeaderValue(), Matchers.equalTo("no-store")); } + + @Test + public void staleIfError() throws Exception { + CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).staleIfError(2, TimeUnit.HOURS); + assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, stale-if-error=7200")); + } + + @Test + public void staleWhileRevalidate() throws Exception { + CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).staleWhileRevalidate(2, TimeUnit.HOURS); + assertThat(cc.getHeaderValue(), Matchers.equalTo("max-age=3600, stale-while-revalidate=7200")); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 9fa07041..77c79da4 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -32,6 +32,7 @@ import java.util.TimeZone; import org.hamcrest.Matchers; import org.junit.Test; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; /** @@ -39,6 +40,7 @@ import static org.junit.Assert.*; * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Brian Clozel */ public class HttpHeadersTests { @@ -46,6 +48,13 @@ public class HttpHeadersTests { @Test + public void getFirst() { + headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public"); + headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000"); + assertThat(headers.getFirst(HttpHeaders.CACHE_CONTROL), is("max-age=1000, public")); + } + + @Test public void accept() { MediaType mediaType1 = new MediaType("text", "html"); MediaType mediaType2 = new MediaType("text", "plain"); @@ -58,13 +67,22 @@ public class HttpHeadersTests { } @Test // SPR-9655 - public void acceptIPlanet() { + public void acceptWithMultipleHeaderValues() { headers.add("Accept", "text/html"); headers.add("Accept", "text/plain"); List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "plain")); assertEquals("Invalid Accept header", expected, headers.getAccept()); } + @Test // SPR-14506 + public void acceptWithMultipleCommaSeparatedHeaderValues() { + headers.add("Accept", "text/html,text/pdf"); + headers.add("Accept", "text/plain,text/csv"); + List<MediaType> expected = Arrays.asList(new MediaType("text", "html"), new MediaType("text", "pdf"), + new MediaType("text", "plain"), new MediaType("text", "csv")); + assertEquals("Invalid Accept header", expected, headers.getAccept()); + } + @Test public void acceptCharsets() { Charset charset1 = Charset.forName("UTF-8"); @@ -133,6 +151,29 @@ public class HttpHeadersTests { } @Test + public void ifMatch() { + String ifMatch = "\"v2.6\""; + headers.setIfMatch(ifMatch); + assertEquals("Invalid If-Match header", ifMatch, headers.getIfMatch().get(0)); + assertEquals("Invalid If-Match header", "\"v2.6\"", headers.getFirst("If-Match")); + } + + @Test(expected = IllegalArgumentException.class) + public void ifMatchIllegalHeader() { + headers.setIfMatch("Illegal"); + headers.getIfMatch(); + } + + @Test + public void ifMatchMultipleHeaders() { + headers.add(HttpHeaders.IF_MATCH, "\"v2,0\""); + headers.add(HttpHeaders.IF_MATCH, "W/\"v2,1\", \"v2,2\""); + assertEquals("Invalid If-Match header", "\"v2,0\"", headers.get(HttpHeaders.IF_MATCH).get(0)); + assertEquals("Invalid If-Match header", "W/\"v2,1\", \"v2,2\"", headers.get(HttpHeaders.IF_MATCH).get(1)); + assertThat(headers.getIfMatch(), Matchers.contains("\"v2,0\"", "W/\"v2,1\"", "\"v2,2\"")); + } + + @Test public void ifNoneMatch() { String ifNoneMatch = "\"v2.6\""; headers.setIfNoneMatch(ifNoneMatch); @@ -141,15 +182,23 @@ public class HttpHeadersTests { } @Test + public void ifNoneMatchWildCard() { + String ifNoneMatch = "*"; + headers.setIfNoneMatch(ifNoneMatch); + assertEquals("Invalid If-None-Match header", ifNoneMatch, headers.getIfNoneMatch().get(0)); + assertEquals("Invalid If-None-Match header", "*", headers.getFirst("If-None-Match")); + } + + @Test public void ifNoneMatchList() { String ifNoneMatch1 = "\"v2.6\""; - String ifNoneMatch2 = "\"v2.7\""; + String ifNoneMatch2 = "\"v2.7\", \"v2.8\""; List<String> ifNoneMatchList = new ArrayList<String>(2); ifNoneMatchList.add(ifNoneMatch1); ifNoneMatchList.add(ifNoneMatch2); headers.setIfNoneMatch(ifNoneMatchList); - assertEquals("Invalid If-None-Match header", ifNoneMatchList, headers.getIfNoneMatch()); - assertEquals("Invalid If-None-Match header", "\"v2.6\", \"v2.7\"", headers.getFirst("If-None-Match")); + assertThat(headers.getIfNoneMatch(), Matchers.contains("\"v2.6\"", "\"v2.7\"", "\"v2.8\"")); + assertEquals("Invalid If-None-Match header", "\"v2.6\", \"v2.7\", \"v2.8\"", headers.getFirst("If-None-Match")); } @Test @@ -256,6 +305,13 @@ public class HttpHeadersTests { } @Test + public void cacheControlAllValues() { + headers.add(HttpHeaders.CACHE_CONTROL, "max-age=1000, public"); + headers.add(HttpHeaders.CACHE_CONTROL, "s-maxage=1000"); + assertThat(headers.getCacheControl(), is("max-age=1000, public, s-maxage=1000")); + } + + @Test public void contentDisposition() { headers.setContentDispositionFormData("name", null); assertEquals("Invalid Content-Disposition header", "form-data; name=\"name\"", @@ -291,6 +347,16 @@ public class HttpHeadersTests { } @Test + public void accessControlAllowHeadersMultipleValues() { + List<String> allowedHeaders = headers.getAccessControlAllowHeaders(); + assertThat(allowedHeaders, Matchers.emptyCollectionOf(String.class)); + headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "header1, header2"); + headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "header3"); + allowedHeaders = headers.getAccessControlAllowHeaders(); + assertEquals(Arrays.asList("header1", "header2", "header3"), allowedHeaders); + } + + @Test public void accessControlAllowMethods() { List<HttpMethod> allowedMethods = headers.getAccessControlAllowMethods(); assertThat(allowedMethods, Matchers.emptyCollectionOf(HttpMethod.class)); diff --git a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java index 85870b3c..5ddec37a 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpRangeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,19 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http; +import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import org.junit.Test; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.support.ResourceRegion; + import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; /** * Unit tests for {@link HttpRange}. * * @author Rossen Stoyanchev + * @author Brian Clozel */ public class HttpRangeTests { @@ -100,4 +110,39 @@ public class HttpRangeTests { assertEquals("Invalid Range header", "bytes=0-499, 9500-, -500", HttpRange.toString(ranges)); } + @Test + public void toResourceRegion() { + byte[] bytes = "Spring Framework".getBytes(Charset.forName("UTF-8")); + ByteArrayResource resource = new ByteArrayResource(bytes); + HttpRange range = HttpRange.createByteRange(0, 5); + ResourceRegion region = range.toResourceRegion(resource); + assertEquals(resource, region.getResource()); + assertEquals(0L, region.getPosition()); + assertEquals(6L, region.getCount()); + } + + @Test(expected = IllegalArgumentException.class) + public void toResourceRegionInputStreamResource() { + InputStreamResource resource = mock(InputStreamResource.class); + HttpRange range = HttpRange.createByteRange(0, 9); + range.toResourceRegion(resource); + } + + @Test(expected = IllegalArgumentException.class) + public void toResourceRegionIllegalLength() { + ByteArrayResource resource = mock(ByteArrayResource.class); + given(resource.contentLength()).willReturn(-1L); + HttpRange range = HttpRange.createByteRange(0, 9); + range.toResourceRegion(resource); + } + + @Test(expected = IllegalArgumentException.class) + @SuppressWarnings("unchecked") + public void toResourceRegionExceptionLength() { + ByteArrayResource resource = mock(ByteArrayResource.class); + given(resource.contentLength()).willThrow(IOException.class); + HttpRange range = HttpRange.createByteRange(0, 9); + range.toResourceRegion(resource); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java b/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java index 574f02bd..3624ece7 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpStatusTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,6 +85,7 @@ public class HttpStatusTests { statusCodes.put(428, "PRECONDITION_REQUIRED"); statusCodes.put(429, "TOO_MANY_REQUESTS"); statusCodes.put(431, "REQUEST_HEADER_FIELDS_TOO_LARGE"); + statusCodes.put(451, "UNAVAILABLE_FOR_LEGAL_REASONS"); statusCodes.put(500, "INTERNAL_SERVER_ERROR"); statusCodes.put(501, "NOT_IMPLEMENTED"); diff --git a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java index 99665b93..ca31a975 100644 --- a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,7 +138,7 @@ public class MediaTypeTests { assertNotNull("No media types returned", mediaTypes); assertEquals("Invalid amount of media types", 4, mediaTypes.size()); - mediaTypes = MediaType.parseMediaTypes(null); + mediaTypes = MediaType.parseMediaTypes(""); assertNotNull("No media types returned", mediaTypes); assertEquals("Invalid amount of media types", 0, mediaTypes.size()); } diff --git a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java index de235366..9b8268d5 100644 --- a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java +++ b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,14 @@ package org.springframework.http; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Test; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.web.util.UriTemplate; import static org.junit.Assert.*; @@ -148,4 +151,14 @@ public class RequestEntityTests { } + @Test // SPR-13154 + public void types() throws URISyntaxException { + URI url = new URI("http://example.com"); + List<String> body = Arrays.asList("foo", "bar"); + ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<String>>() {}; + + RequestEntity<?> entity = RequestEntity.post(url).body(body, typeReference.getType()); + assertEquals(typeReference.getType(), entity.getType()); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java b/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java index dca351a4..5bc9d3f7 100644 --- a/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java +++ b/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -252,4 +252,22 @@ public class ResponseEntityTests { assertThat(cacheControlHeader, Matchers.equalTo("no-store")); } + @Test + public void statusCodeAsInt() { + Integer entity = new Integer(42); + ResponseEntity<Integer> responseEntity = ResponseEntity.status(200).body(entity); + + assertEquals(200, responseEntity.getStatusCode().value()); + assertEquals(entity, responseEntity.getBody()); + } + + @Test + public void customStatusCode() { + Integer entity = new Integer(42); + ResponseEntity<Integer> responseEntity = ResponseEntity.status(299).body(entity); + + assertEquals(299, responseEntity.getStatusCodeValue()); + assertEquals(entity, responseEntity.getBody()); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java index 8b70e32f..1f97af02 100644 --- a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,19 @@ package org.springframework.http.client; -import static org.junit.Assert.assertEquals; - +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import org.junit.Test; + import org.springframework.http.HttpMethod; +import static org.junit.Assert.*; + public class BufferedSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { @Override @@ -89,5 +92,11 @@ public class BufferedSimpleHttpRequestFactoryTests extends AbstractHttpRequestFa public boolean usingProxy() { return false; } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(new byte[0]); + } } + } diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java new file mode 100644 index 00000000..12e857e6 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/OkHttp3AsyncClientHttpRequestFactoryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; + +/** + * @author Roy Clarkson + */ +public class OkHttp3AsyncClientHttpRequestFactoryTests extends AbstractAsyncHttpRequestFactoryTestCase { + + @Override + protected AsyncClientHttpRequestFactory createRequestFactory() { + return new OkHttp3ClientHttpRequestFactory(); + } + + @Override + @Test + public void httpMethods() throws Exception { + super.httpMethods(); + assertHttpMethod("patch", HttpMethod.PATCH); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java new file mode 100644 index 00000000..dd1874ad --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; + +/** + * @author Roy Clarkson + */ +public class OkHttp3ClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { + + @Override + protected ClientHttpRequestFactory createRequestFactory() { + return new OkHttp3ClientHttpRequestFactory(); + } + + @Override + @Test + public void httpMethods() throws Exception { + super.httpMethods(); + assertHttpMethod("patch", HttpMethod.PATCH); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java index f7c367e6..f508027d 100644 --- a/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/OkHttpAsyncClientHttpRequestFactoryTests.java @@ -24,7 +24,7 @@ import org.springframework.http.HttpMethod; * @author Luciano Leggieri */ public class OkHttpAsyncClientHttpRequestFactoryTests extends AbstractAsyncHttpRequestFactoryTestCase { - + @Override protected AsyncClientHttpRequestFactory createRequestFactory() { return new OkHttpClientHttpRequestFactory(); diff --git a/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java new file mode 100644 index 00000000..4be26a26 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpResponseTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.nio.charset.Charset; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.util.StreamUtils; + +/** + * @author Brian Clozel + */ +public class SimpleClientHttpResponseTests { + + private final Charset UTF8 = Charset.forName("UTF-8"); + + private SimpleClientHttpResponse response; + + private HttpURLConnection connection; + + @Before + public void setup() throws Exception { + this.connection = mock(HttpURLConnection.class); + this.response = new SimpleClientHttpResponse(this.connection); + } + + // SPR-14040 + @Test + public void shouldNotCloseConnectionWhenResponseClosed() throws Exception { + TestByteArrayInputStream is = new TestByteArrayInputStream("Spring".getBytes(UTF8)); + given(this.connection.getErrorStream()).willReturn(null); + given(this.connection.getInputStream()).willReturn(is); + + InputStream responseStream = this.response.getBody(); + assertThat(StreamUtils.copyToString(responseStream, UTF8), is("Spring")); + + this.response.close(); + assertTrue(is.isClosed()); + verify(this.connection, never()).disconnect(); + } + + // SPR-14040 + @Test + public void shouldDrainStreamWhenResponseClosed() throws Exception { + byte[] buf = new byte[6]; + TestByteArrayInputStream is = new TestByteArrayInputStream("SpringSpring".getBytes(UTF8)); + given(this.connection.getErrorStream()).willReturn(null); + given(this.connection.getInputStream()).willReturn(is); + + InputStream responseStream = this.response.getBody(); + responseStream.read(buf); + assertThat(new String(buf, UTF8), is("Spring")); + assertThat(is.available(), is(6)); + + this.response.close(); + assertThat(is.available(), is(0)); + assertTrue(is.isClosed()); + verify(this.connection, never()).disconnect(); + } + + // SPR-14040 + @Test + public void shouldDrainErrorStreamWhenResponseClosed() throws Exception { + byte[] buf = new byte[6]; + TestByteArrayInputStream is = new TestByteArrayInputStream("SpringSpring".getBytes(UTF8)); + given(this.connection.getErrorStream()).willReturn(is); + + InputStream responseStream = this.response.getBody(); + responseStream.read(buf); + assertThat(new String(buf, UTF8), is("Spring")); + assertThat(is.available(), is(6)); + + this.response.close(); + assertThat(is.available(), is(0)); + assertTrue(is.isClosed()); + verify(this.connection, never()).disconnect(); + } + + + class TestByteArrayInputStream extends ByteArrayInputStream { + + private boolean closed; + + public TestByteArrayInputStream(byte[] buf) { + super(buf); + this.closed = false; + } + + public boolean isClosed() { + return closed; + } + + @Override + public void close() throws IOException { + super.close(); + this.closed = true; + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java b/spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java new file mode 100644 index 00000000..ce96460d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/support/BasicAuthorizationInterceptorTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client.support; + +import java.net.URI; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.SimpleClientHttpRequestFactory; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link BasicAuthorizationInterceptor}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +public class BasicAuthorizationInterceptorTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenUsernameIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Username must not be empty"); + new BasicAuthorizationInterceptor(null, "password"); + } + + @Test + public void createWhenUsernameIsEmptyShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Username must not be empty"); + new BasicAuthorizationInterceptor("", "password"); + } + + @Test + public void createWhenPasswordIsNullShouldUseEmptyPassword() throws Exception { + BasicAuthorizationInterceptor interceptor = new BasicAuthorizationInterceptor( + "username", null); + assertEquals("", new DirectFieldAccessor(interceptor).getPropertyValue("password")); + } + + @Test + public void interceptShouldAddHeader() throws Exception { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET); + ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); + byte[] body = new byte[] {}; + new BasicAuthorizationInterceptor("spring", "boot").intercept(request, body, + execution); + verify(execution).execute(request, body); + assertEquals("Basic c3ByaW5nOmJvb3Q=", request.getHeaders().getFirst("Authorization")); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index 488e1826..aaa03b59 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -261,7 +261,7 @@ public class FormHttpMessageConverterTests { @Override public String getCharacterEncoding() { MediaType type = this.outputMessage.getHeaders().getContentType(); - return (type != null && type.getCharSet() != null ? type.getCharSet().name() : null); + return (type != null && type.getCharset() != null ? type.getCharset().name() : null); } @Override diff --git a/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java index 10752f22..d3a3d96f 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/ResourceHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,15 @@ package org.springframework.http.converter; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @@ -31,13 +40,10 @@ import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; import org.springframework.util.FileCopyUtils; -import static org.hamcrest.core.Is.*; -import static org.hamcrest.core.IsInstanceOf.*; -import static org.junit.Assert.*; - /** * @author Arjen Poutsma * @author Kazuki Shimizu + * @author Brian Clozel */ public class ResourceHttpMessageConverterTests { @@ -45,18 +51,18 @@ public class ResourceHttpMessageConverterTests { @Test - public void canRead() { + public void canReadResource() { assertTrue(converter.canRead(Resource.class, new MediaType("application", "octet-stream"))); } @Test - public void canWrite() { + public void canWriteResource() { assertTrue(converter.canWrite(Resource.class, new MediaType("application", "octet-stream"))); assertTrue(converter.canWrite(Resource.class, MediaType.ALL)); } @Test - public void read() throws IOException { + public void shouldReadImageResource() throws IOException { byte[] body = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("logo.jpg")); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG); @@ -65,7 +71,7 @@ public class ResourceHttpMessageConverterTests { } @Test // SPR-13443 - public void readWithInputStreamResource() throws IOException { + public void shouldReadInputStreamResource() throws IOException { try (InputStream body = getClass().getResourceAsStream("logo.jpg") ) { MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); inputMessage.getHeaders().setContentType(MediaType.IMAGE_JPEG); @@ -76,7 +82,7 @@ public class ResourceHttpMessageConverterTests { } @Test - public void write() throws IOException { + public void shouldWriteImageResource() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); Resource body = new ClassPathResource("logo.jpg", getClass()); converter.write(body, null, outputMessage); @@ -94,4 +100,45 @@ public class ResourceHttpMessageConverterTests { assertTrue(Arrays.equals(byteArray, outputMessage.getBodyAsBytes())); } + // SPR-12999 + @Test @SuppressWarnings("unchecked") + public void writeContentNotGettingInputStream() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource resource = mock(Resource.class); + given(resource.getInputStream()).willThrow(FileNotFoundException.class); + + converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage); + + assertEquals(0, outputMessage.getHeaders().getContentLength()); + } + + // SPR-12999 + @Test + public void writeContentNotClosingInputStream() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource resource = mock(Resource.class); + InputStream inputStream = mock(InputStream.class); + given(resource.getInputStream()).willReturn(inputStream); + given(inputStream.read(any())).willReturn(-1); + doThrow(new NullPointerException()).when(inputStream).close(); + + converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage); + + assertEquals(0, outputMessage.getHeaders().getContentLength()); + } + + // SPR-13620 + @Test @SuppressWarnings("unchecked") + public void writeContentInputStreamThrowingNullPointerException() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource resource = mock(Resource.class); + InputStream in = mock(InputStream.class); + given(resource.getInputStream()).willReturn(in); + given(in.read(any())).willThrow(NullPointerException.class); + + converter.write(resource, MediaType.APPLICATION_OCTET_STREAM, outputMessage); + + assertEquals(0, outputMessage.getHeaders().getContentLength()); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java new file mode 100644 index 00000000..a4f9c465 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/ResourceRegionHttpMessageConverterTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matchers; +import org.junit.Test; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourceRegion; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRange; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.util.StringUtils; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Test cases for {@link ResourceRegionHttpMessageConverter} class. + * + * @author Brian Clozel + */ +public class ResourceRegionHttpMessageConverterTests { + + private final ResourceRegionHttpMessageConverter converter = new ResourceRegionHttpMessageConverter(); + + @Test + public void canReadResource() { + assertFalse(converter.canRead(Resource.class, MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(converter.canRead(Resource.class, MediaType.ALL)); + assertFalse(converter.canRead(List.class, MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(converter.canRead(List.class, MediaType.ALL)); + } + + @Test + public void canWriteResource() { + assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.APPLICATION_OCTET_STREAM)); + assertTrue(converter.canWrite(ResourceRegion.class, null, MediaType.ALL)); + } + + @Test + public void canWriteResourceCollection() { + Type resourceRegionList = new ParameterizedTypeReference<List<ResourceRegion>>() {}.getType(); + assertTrue(converter.canWrite(resourceRegionList, null, MediaType.APPLICATION_OCTET_STREAM)); + assertTrue(converter.canWrite(resourceRegionList, null, MediaType.ALL)); + + assertFalse(converter.canWrite(List.class, MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(converter.canWrite(List.class, MediaType.ALL)); + } + + @Test + public void shouldWritePartialContentByteRange() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource body = new ClassPathResource("byterangeresource.txt", getClass()); + ResourceRegion region = HttpRange.createByteRange(0, 5).toResourceRegion(body); + converter.write(region, MediaType.TEXT_PLAIN, outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); + assertThat(headers.getContentLength(), is(6L)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 0-5/39")); + assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Spring")); + } + + @Test + public void shouldWritePartialContentByteRangeNoEnd() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource body = new ClassPathResource("byterangeresource.txt", getClass()); + ResourceRegion region = HttpRange.createByteRange(7).toResourceRegion(body); + converter.write(region, MediaType.TEXT_PLAIN, outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + assertThat(headers.getContentType(), is(MediaType.TEXT_PLAIN)); + assertThat(headers.getContentLength(), is(32L)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).size(), is(1)); + assertThat(headers.get(HttpHeaders.CONTENT_RANGE).get(0), is("bytes 7-38/39")); + assertThat(outputMessage.getBodyAsString(Charset.forName("UTF-8")), is("Framework test resource content.")); + } + + @Test + public void partialContentMultipleByteRanges() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Resource body = new ClassPathResource("byterangeresource.txt", getClass()); + List<HttpRange> rangeList = HttpRange.parseRanges("bytes=0-5,7-15,17-20,22-38"); + List<ResourceRegion> regions = new ArrayList<ResourceRegion>(); + for(HttpRange range : rangeList) { + regions.add(range.toResourceRegion(body)); + } + + converter.write(regions, MediaType.TEXT_PLAIN, outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + assertThat(headers.getContentType().toString(), Matchers.startsWith("multipart/byteranges;boundary=")); + String boundary = "--" + headers.getContentType().toString().substring(30); + String content = outputMessage.getBodyAsString(Charset.forName("UTF-8")); + String[] ranges = StringUtils.tokenizeToStringArray(content, "\r\n", false, true); + + assertThat(ranges[0], is(boundary)); + assertThat(ranges[1], is("Content-Type: text/plain")); + assertThat(ranges[2], is("Content-Range: bytes 0-5/39")); + assertThat(ranges[3], is("Spring")); + + assertThat(ranges[4], is(boundary)); + assertThat(ranges[5], is("Content-Type: text/plain")); + assertThat(ranges[6], is("Content-Range: bytes 7-15/39")); + assertThat(ranges[7], is("Framework")); + + assertThat(ranges[8], is(boundary)); + assertThat(ranges[9], is("Content-Type: text/plain")); + assertThat(ranges[10], is("Content-Range: bytes 17-20/39")); + assertThat(ranges[11], is("test")); + + assertThat(ranges[12], is(boundary)); + assertThat(ranges[13], is("Content-Type: text/plain")); + assertThat(ranges[14], is("Content-Range: bytes 22-38/39")); + assertThat(ranges[15], is("resource content.")); + } + +}
\ No newline at end of file diff --git a/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java index 6dcdb25f..2fa2a574 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,86 +22,97 @@ import java.nio.charset.Charset; import org.junit.Before; import org.junit.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; import static org.junit.Assert.*; -/** @author Arjen Poutsma */ +/** + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ public class StringHttpMessageConverterTests { + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + public static final MediaType TEXT_PLAIN_UTF_8 = new MediaType("text", "plain", UTF_8); + + private StringHttpMessageConverter converter; + private MockHttpOutputMessage outputMessage; + + @Before public void setUp() { - converter = new StringHttpMessageConverter(); + this.converter = new StringHttpMessageConverter(); + this.outputMessage = new MockHttpOutputMessage(); } + @Test public void canRead() { - assertTrue(converter.canRead(String.class, new MediaType("text", "plain"))); + assertTrue(this.converter.canRead(String.class, MediaType.TEXT_PLAIN)); } @Test public void canWrite() { - assertTrue(converter.canWrite(String.class, new MediaType("text", "plain"))); - assertTrue(converter.canWrite(String.class, MediaType.ALL)); + assertTrue(this.converter.canWrite(String.class, MediaType.TEXT_PLAIN)); + assertTrue(this.converter.canWrite(String.class, MediaType.ALL)); } @Test public void read() throws IOException { String body = "Hello World"; - Charset charset = Charset.forName("UTF-8"); - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); - inputMessage.getHeaders().setContentType(new MediaType("text", "plain", charset)); - String result = converter.read(String.class, inputMessage); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(UTF_8)); + inputMessage.getHeaders().setContentType(TEXT_PLAIN_UTF_8); + String result = this.converter.read(String.class, inputMessage); + assertEquals("Invalid result", body, result); } @Test public void writeDefaultCharset() throws IOException { Charset iso88591 = Charset.forName("ISO-8859-1"); - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); String body = "H\u00e9llo W\u00f6rld"; - converter.write(body, null, outputMessage); - assertEquals("Invalid result", body, outputMessage.getBodyAsString(iso88591)); - assertEquals("Invalid content-type", new MediaType("text", "plain", iso88591), - outputMessage.getHeaders().getContentType()); - assertEquals("Invalid content-length", body.getBytes(iso88591).length, - outputMessage.getHeaders().getContentLength()); - assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty()); + this.converter.write(body, null, this.outputMessage); + + HttpHeaders headers = this.outputMessage.getHeaders(); + assertEquals(body, this.outputMessage.getBodyAsString(iso88591)); + assertEquals(new MediaType("text", "plain", iso88591), headers.getContentType()); + assertEquals(body.getBytes(iso88591).length, headers.getContentLength()); + assertFalse(headers.getAcceptCharset().isEmpty()); } @Test public void writeUTF8() throws IOException { - Charset utf8 = Charset.forName("UTF-8"); - MediaType contentType = new MediaType("text", "plain", utf8); - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); String body = "H\u00e9llo W\u00f6rld"; - converter.write(body, contentType, outputMessage); - assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8)); - assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType()); - assertEquals("Invalid content-length", body.getBytes(utf8).length, - outputMessage.getHeaders().getContentLength()); - assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty()); + this.converter.write(body, TEXT_PLAIN_UTF_8, this.outputMessage); + + HttpHeaders headers = this.outputMessage.getHeaders(); + assertEquals(body, this.outputMessage.getBodyAsString(UTF_8)); + assertEquals(TEXT_PLAIN_UTF_8, headers.getContentType()); + assertEquals(body.getBytes(UTF_8).length, headers.getContentLength()); + assertFalse(headers.getAcceptCharset().isEmpty()); } // SPR-8867 @Test public void writeOverrideRequestedContentType() throws IOException { - Charset utf8 = Charset.forName("UTF-8"); - MediaType requestedContentType = new MediaType("text", "html"); - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - MediaType contentType = new MediaType("text", "plain", utf8); - outputMessage.getHeaders().setContentType(contentType); String body = "H\u00e9llo W\u00f6rld"; - converter.write(body, requestedContentType, outputMessage); - assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8)); - assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType()); - assertEquals("Invalid content-length", body.getBytes(utf8).length, - outputMessage.getHeaders().getContentLength()); - assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty()); + MediaType requestedContentType = new MediaType("text", "html"); + + HttpHeaders headers = this.outputMessage.getHeaders(); + headers.setContentType(TEXT_PLAIN_UTF_8); + this.converter.write(body, requestedContentType, this.outputMessage); + + assertEquals(body, this.outputMessage.getBodyAsString(UTF_8)); + assertEquals(TEXT_PLAIN_UTF_8, headers.getContentType()); + assertEquals(body.getBytes(UTF_8).length, headers.getContentLength()); + assertFalse(headers.getAcceptCharset().isEmpty()); } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java index bcaa76b9..54d83074 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/feed/AtomFeedHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,13 +42,13 @@ import static org.junit.Assert.assertTrue; */ public class AtomFeedHttpMessageConverterTests { + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private AtomFeedHttpMessageConverter converter; - private Charset utf8; @Before public void setUp() { - utf8 = Charset.forName("UTF-8"); converter = new AtomFeedHttpMessageConverter(); XMLUnit.setIgnoreWhitespace(true); } @@ -56,20 +56,20 @@ public class AtomFeedHttpMessageConverterTests { @Test public void canRead() { assertTrue(converter.canRead(Feed.class, new MediaType("application", "atom+xml"))); - assertTrue(converter.canRead(Feed.class, new MediaType("application", "atom+xml", utf8))); + assertTrue(converter.canRead(Feed.class, new MediaType("application", "atom+xml", UTF_8))); } @Test public void canWrite() { assertTrue(converter.canWrite(Feed.class, new MediaType("application", "atom+xml"))); - assertTrue(converter.canWrite(Feed.class, new MediaType("application", "atom+xml", Charset.forName("UTF-8")))); + assertTrue(converter.canWrite(Feed.class, new MediaType("application", "atom+xml", UTF_8))); } @Test public void read() throws IOException { InputStream is = getClass().getResourceAsStream("atom.xml"); MockHttpInputMessage inputMessage = new MockHttpInputMessage(is); - inputMessage.getHeaders().setContentType(new MediaType("application", "atom+xml", utf8)); + inputMessage.getHeaders().setContentType(new MediaType("application", "atom+xml", UTF_8)); Feed result = converter.read(Feed.class, inputMessage); assertEquals("title", result.getTitle()); assertEquals("subtitle", result.getSubtitle().getValue()); @@ -106,12 +106,12 @@ public class AtomFeedHttpMessageConverterTests { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); converter.write(feed, null, outputMessage); - assertEquals("Invalid content-type", new MediaType("application", "atom+xml", utf8), + assertEquals("Invalid content-type", new MediaType("application", "atom+xml", UTF_8), outputMessage.getHeaders().getContentType()); String expected = "<feed xmlns=\"http://www.w3.org/2005/Atom\">" + "<title>title</title>" + "<entry><id>id1</id><title>title1</title></entry>" + "<entry><id>id2</id><title>title2</title></entry></feed>"; - assertXMLEqual(expected, outputMessage.getBodyAsString(utf8)); + assertXMLEqual(expected, outputMessage.getBodyAsString(UTF_8)); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java index 16fcfc00..a8ceeb3b 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/feed/RssChannelHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,34 +42,35 @@ import static org.junit.Assert.assertTrue; */ public class RssChannelHttpMessageConverterTests { + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private RssChannelHttpMessageConverter converter; - private Charset utf8; @Before public void setUp() { - utf8 = Charset.forName("UTF-8"); converter = new RssChannelHttpMessageConverter(); XMLUnit.setIgnoreWhitespace(true); } + @Test public void canRead() { assertTrue(converter.canRead(Channel.class, new MediaType("application", "rss+xml"))); - assertTrue(converter.canRead(Channel.class, new MediaType("application", "rss+xml", utf8))); + assertTrue(converter.canRead(Channel.class, new MediaType("application", "rss+xml", UTF_8))); } @Test public void canWrite() { assertTrue(converter.canWrite(Channel.class, new MediaType("application", "rss+xml"))); - assertTrue(converter.canWrite(Channel.class, new MediaType("application", "rss+xml", Charset.forName("UTF-8")))); + assertTrue(converter.canWrite(Channel.class, new MediaType("application", "rss+xml", UTF_8))); } @Test public void read() throws IOException { InputStream is = getClass().getResourceAsStream("rss.xml"); MockHttpInputMessage inputMessage = new MockHttpInputMessage(is); - inputMessage.getHeaders().setContentType(new MediaType("application", "rss+xml", utf8)); + inputMessage.getHeaders().setContentType(new MediaType("application", "rss+xml", UTF_8)); Channel result = converter.read(Channel.class, inputMessage); assertEquals("title", result.getTitle()); assertEquals("http://example.com", result.getLink()); @@ -106,14 +107,14 @@ public class RssChannelHttpMessageConverterTests { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); converter.write(channel, null, outputMessage); - assertEquals("Invalid content-type", new MediaType("application", "rss+xml", utf8), + assertEquals("Invalid content-type", new MediaType("application", "rss+xml", UTF_8), outputMessage.getHeaders().getContentType()); String expected = "<rss version=\"2.0\">" + "<channel><title>title</title><link>http://example.com</link><description>description</description>" + "<item><title>title1</title></item>" + "<item><title>title2</title></item>" + "</channel></rss>"; - assertXMLEqual(expected, outputMessage.getBodyAsString(utf8)); + assertXMLEqual(expected, outputMessage.getBodyAsString(UTF_8)); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index 0f02ad21..ddb9e1e6 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -62,7 +64,7 @@ import com.fasterxml.jackson.databind.ser.std.ClassSerializer; import com.fasterxml.jackson.databind.ser.std.NumberSerializer; import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.dataformat.xml.XmlMapper; - +import kotlin.ranges.IntRange; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.Test; @@ -77,6 +79,7 @@ import static org.junit.Assert.*; * * @author Sebastien Deleuze */ +@SuppressWarnings("deprecation") public class Jackson2ObjectMapperBuilderTests { private static final String DATE_FORMAT = "yyyy-MM-dd"; @@ -249,13 +252,17 @@ public class Jackson2ObjectMapperBuilderTests { assertEquals(timestamp.toString(), new String(objectMapper.writeValueAsBytes(dateTime), "UTF-8")); Path file = Paths.get("foo"); - assertEquals("\"foo\"", new String(objectMapper.writeValueAsBytes(file), "UTF-8")); + assertTrue(new String(objectMapper.writeValueAsBytes(file), "UTF-8").endsWith("foo\"")); Optional<String> optional = Optional.of("test"); assertEquals("\"test\"", new String(objectMapper.writeValueAsBytes(optional), "UTF-8")); + + // Kotlin module + IntRange range = new IntRange(1, 3); + assertEquals("{\"start\":1,\"end\":3}", new String(objectMapper.writeValueAsBytes(range), "UTF-8")); } - @Test // SPR-12634 + @Test // SPR-12634 public void customizeWellKnownModulesWithModule() throws JsonProcessingException, UnsupportedEncodingException { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() .modulesToInstall(new CustomIntegerModule()).build(); @@ -264,7 +271,7 @@ public class Jackson2ObjectMapperBuilderTests { assertThat(new String(objectMapper.writeValueAsBytes(new Integer(4)), "UTF-8"), containsString("customid")); } - @Test // SPR-12634 + @Test // SPR-12634 @SuppressWarnings("unchecked") public void customizeWellKnownModulesWithModuleClass() throws JsonProcessingException, UnsupportedEncodingException { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modulesToInstall(CustomIntegerModule.class).build(); @@ -273,7 +280,7 @@ public class Jackson2ObjectMapperBuilderTests { assertThat(new String(objectMapper.writeValueAsBytes(new Integer(4)), "UTF-8"), containsString("customid")); } - @Test // SPR-12634 + @Test // SPR-12634 public void customizeWellKnownModulesWithSerializer() throws JsonProcessingException, UnsupportedEncodingException { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() .serializerByType(Integer.class, new CustomIntegerSerializer()).build(); @@ -326,8 +333,8 @@ public class Jackson2ObjectMapperBuilderTests { Class<?> target = String.class; Class<?> mixInSource = Object.class; - ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().mixIn(target, - mixInSource).build(); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules() + .mixIn(target, mixInSource).build(); assertEquals(1, objectMapper.mixInCount()); assertSame(mixInSource, objectMapper.findMixInClassFor(target)); @@ -340,7 +347,8 @@ public class Jackson2ObjectMapperBuilderTests { Map<Class<?>, Class<?>> mixIns = new HashMap<Class<?>, Class<?>>(); mixIns.put(target, mixInSource); - ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().mixIns(mixIns).build(); + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules() + .mixIns(mixIns).build(); assertEquals(1, objectMapper.mixInCount()); assertSame(mixInSource, objectMapper.findMixInClassFor(target)); @@ -437,6 +445,16 @@ public class Jackson2ObjectMapperBuilderTests { assertTrue(xmlObjectMapper.getClass().isAssignableFrom(XmlMapper.class)); } + @Test // SPR-13975 + public void defaultUseWrapper() throws JsonProcessingException { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().defaultUseWrapper(false).build(); + assertNotNull(objectMapper); + assertEquals(XmlMapper.class, objectMapper.getClass()); + ListContainer<String> container = new ListContainer<>(Arrays.asList("foo", "bar")); + String output = objectMapper.writeValueAsString(container); + assertThat(output, containsString("<list>foo</list><list>bar</list></ListContainer>")); + } + public static class CustomIntegerModule extends Module { @@ -501,4 +519,25 @@ public class Jackson2ObjectMapperBuilderTests { } } + + public static class ListContainer<T> { + + private List<T> list; + + public ListContainer() { + } + + public ListContainer(List<T> list) { + this.list = list; + } + + public List<T> getList() { + return list; + } + + public void setList(List<T> list) { + this.list = list; + } + } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java index 005623f6..d678c660 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,7 @@ import static org.junit.Assert.*; * @author Sebastien Deleuze * @author Sam Brannen */ +@SuppressWarnings("deprecation") public class Jackson2ObjectMapperFactoryBeanTests { private static final String DATE_FORMAT = "yyyy-MM-dd"; @@ -285,6 +286,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { Map<Class<?>, Class<?>> mixIns = new HashMap<Class<?>, Class<?>>(); mixIns.put(target, mixinSource); + this.factory.setModules(Collections.emptyList()); this.factory.setMixIns(mixIns); this.factory.afterPropertiesSet(); ObjectMapper objectMapper = this.factory.getObject(); diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java index 687df9c9..2f334ea9 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/SpringHandlerInstantiatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,6 @@ import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder; import com.fasterxml.jackson.databind.type.TypeFactory; - import org.junit.Before; import org.junit.Test; @@ -197,7 +196,7 @@ public class SpringHandlerInstantiatorTests { return JsonTypeInfo.Id.CUSTOM; } - @Override + // Only needed when compiling against Jackson 2.7; gone in 2.8 @SuppressWarnings("deprecation") public JavaType typeFromId(String s) { return TypeFactory.defaultInstance().constructFromCanonical(s); @@ -218,10 +217,15 @@ public class SpringHandlerInstantiatorTests { return null; } - // New in Jackson 2.5 + @Override public JavaType typeFromId(DatabindContext context, String id) { return null; } + + // New in Jackson 2.7 + public String getDescForKnownTypeIds() { + return null; + } } diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java index 4b8e60fe..b592a045 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,12 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.util.FileCopyUtils; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller */ public class ServletServerHttpResponseTests { @@ -79,7 +78,6 @@ public class ServletServerHttpResponseTests { @Test public void preExistingHeadersFromHttpServletResponse() { - String headerName = "Access-Control-Allow-Origin"; String headerValue = "localhost:8080"; @@ -89,6 +87,8 @@ public class ServletServerHttpResponseTests { assertEquals(headerValue, this.response.getHeaders().getFirst(headerName)); assertEquals(Collections.singletonList(headerValue), this.response.getHeaders().get(headerName)); assertTrue(this.response.getHeaders().containsKey(headerName)); + assertEquals(headerValue, this.response.getHeaders().getFirst(headerName)); + assertEquals(headerValue, this.response.getHeaders().getAccessControlAllowOrigin()); } @Test @@ -98,4 +98,5 @@ public class ServletServerHttpResponseTests { assertArrayEquals("Invalid content written", content, mockResponse.getContentAsByteArray()); } -}
\ No newline at end of file + +} diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java index 48b03112..da21c6e2 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletRequest.java @@ -390,8 +390,8 @@ public class MockHttpServletRequest implements HttpServletRequest { if (contentType != null) { try { MediaType mediaType = MediaType.parseMediaType(contentType); - if (mediaType.getCharSet() != null) { - this.characterEncoding = mediaType.getCharSet().name(); + if (mediaType.getCharset() != null) { + this.characterEncoding = mediaType.getCharset().name(); } } catch (Exception ex) { diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java index 413b3321..c0986b29 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockHttpServletResponse.java @@ -236,8 +236,8 @@ public class MockHttpServletResponse implements HttpServletResponse { if (contentType != null) { try { MediaType mediaType = MediaType.parseMediaType(contentType); - if (mediaType.getCharSet() != null) { - this.characterEncoding = mediaType.getCharSet().name(); + if (mediaType.getCharset() != null) { + this.characterEncoding = mediaType.getCharset().name(); this.charset = true; } } diff --git a/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java b/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java index 224374c0..acaef598 100644 --- a/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java +++ b/spring-web/src/test/java/org/springframework/mock/web/test/MockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.Enumeration; import java.util.EventListener; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -144,7 +143,7 @@ public class MockServletContext implements ServletContext { private String servletContextName = "MockServletContext"; - private final Set<String> declaredRoles = new HashSet<String>(); + private final Set<String> declaredRoles = new LinkedHashSet<String>(); private Set<SessionTrackingMode> sessionTrackingModes; @@ -370,7 +369,6 @@ public class MockServletContext implements ServletContext { /** * Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher}) * that acts as a wrapper for the named Servlet. - * * @param name the name of the wrapped Servlet * @param requestDispatcher the dispatcher that wraps the named Servlet * @see #getNamedDispatcher @@ -384,7 +382,6 @@ public class MockServletContext implements ServletContext { /** * Unregister the {@link RequestDispatcher} with the given name. - * * @param name the name of the dispatcher to unregister * @see #getNamedDispatcher * @see #registerNamedDispatcher @@ -429,13 +426,13 @@ public class MockServletContext implements ServletContext { @Override @Deprecated public Enumeration<Servlet> getServlets() { - return Collections.enumeration(new HashSet<Servlet>()); + return Collections.enumeration(Collections.<Servlet>emptySet()); } @Override @Deprecated public Enumeration<String> getServletNames() { - return Collections.enumeration(new HashSet<String>()); + return Collections.enumeration(Collections.<String>emptySet()); } @Override diff --git a/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java b/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java index 3a887601..1376734f 100644 --- a/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java +++ b/spring-web/src/test/java/org/springframework/remoting/jaxws/JaxWsSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,7 +127,7 @@ public class JaxWsSupportTests { } catch (BeanCreationException ex) { if ("exporter".equals(ex.getBeanName()) && ex.getRootCause() instanceof ClassNotFoundException) { - // ignore - probably running on JDK < 1.6 without the JAX-WS impl present + // ignore - probably running on JDK without the JAX-WS impl present } else { throw ex; @@ -146,7 +146,7 @@ public class JaxWsSupportTests { public OrderService myService; - @WebServiceRef(value=OrderServiceService.class, wsdlLocation = "http://localhost:9999/OrderService?wsdl") + @WebServiceRef(value = OrderServiceService.class, wsdlLocation = "http://localhost:9999/OrderService?wsdl") public void setMyService(OrderService myService) { this.myService = myService; } diff --git a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java index e9d95c2a..baabc764 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java @@ -30,10 +30,11 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * Test fixture for {@link ContentNegotiationManagerFactoryBean} tests. + * * @author Rossen Stoyanchev */ public class ContentNegotiationManagerFactoryBeanTests { @@ -119,9 +120,7 @@ public class ContentNegotiationManagerFactoryBeanTests { assertEquals(Collections.emptyList(), manager.resolveMediaTypes(this.webRequest)); } - // SPR-10170 - - @Test(expected = HttpMediaTypeNotAcceptableException.class) + @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170 public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception { this.factoryBean.setFavorPathExtension(true); this.factoryBean.setIgnoreUnknownPathExtensions(false); @@ -152,9 +151,7 @@ public class ContentNegotiationManagerFactoryBeanTests { manager.resolveMediaTypes(this.webRequest)); } - // SPR-10170 - - @Test(expected = HttpMediaTypeNotAcceptableException.class) + @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170 public void favorParameterWithUnknownMediaType() throws HttpMediaTypeNotAcceptableException { this.factoryBean.setFavorParameter(true); this.factoryBean.afterPropertiesSet(); @@ -188,16 +185,12 @@ public class ContentNegotiationManagerFactoryBeanTests { manager.resolveMediaTypes(this.webRequest)); // SPR-10513 - this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE); - assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), manager.resolveMediaTypes(this.webRequest)); } - // SPR-12286 - - @Test + @Test // SPR-12286 public void setDefaultContentTypeWithStrategy() throws Exception { this.factoryBean.setDefaultContentTypeStrategy(new FixedContentNegotiationStrategy(MediaType.APPLICATION_JSON)); this.factoryBean.afterPropertiesSet(); @@ -216,7 +209,6 @@ public class ContentNegotiationManagerFactoryBeanTests { private final Map<String, String> mimeTypes = new HashMap<>(); - public Map<String, String> getMimeTypes() { return this.mimeTypes; } diff --git a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java index 72f8aca9..f6fa0d6b 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.accept; import java.util.List; -import org.junit.Before; import org.junit.Test; import org.springframework.http.MediaType; @@ -32,21 +32,16 @@ import static org.junit.Assert.*; * Test fixture for HeaderContentNegotiationStrategy tests. * * @author Rossen Stoyanchev + * @author Juergen Hoeller */ public class HeaderContentNegotiationStrategyTests { - private HeaderContentNegotiationStrategy strategy; + private final HeaderContentNegotiationStrategy strategy = new HeaderContentNegotiationStrategy(); - private NativeWebRequest webRequest; + private final MockHttpServletRequest servletRequest = new MockHttpServletRequest(); - private MockHttpServletRequest servletRequest; + private final NativeWebRequest webRequest = new ServletWebRequest(this.servletRequest); - @Before - public void setup() { - this.strategy = new HeaderContentNegotiationStrategy(); - this.servletRequest = new MockHttpServletRequest(); - this.webRequest = new ServletWebRequest(servletRequest ); - } @Test public void resolveMediaTypes() throws Exception { @@ -60,7 +55,20 @@ public class HeaderContentNegotiationStrategyTests { assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString()); } - @Test(expected=HttpMediaTypeNotAcceptableException.class) + @Test // SPR-14506 + public void resolveMediaTypesFromMultipleHeaderValues() throws Exception { + this.servletRequest.addHeader("Accept", "text/plain; q=0.5, text/html"); + this.servletRequest.addHeader("Accept", "text/x-dvi; q=0.8, text/x-c"); + List<MediaType> mediaTypes = this.strategy.resolveMediaTypes(this.webRequest); + + assertEquals(4, mediaTypes.size()); + assertEquals("text/html", mediaTypes.get(0).toString()); + assertEquals("text/x-c", mediaTypes.get(1).toString()); + assertEquals("text/x-dvi;q=0.8", mediaTypes.get(2).toString()); + assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString()); + } + + @Test(expected = HttpMediaTypeNotAcceptableException.class) public void resolveMediaTypesParseError() throws Exception { this.servletRequest.addHeader("Accept", "textplain; q=0.5"); this.strategy.resolveMediaTypes(this.webRequest); diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java index 32b4c0e1..508be6d3 100644 --- a/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java +++ b/spring-web/src/test/java/org/springframework/web/bind/support/WebRequestDataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,15 @@ package org.springframework.web.bind.support; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + import java.beans.PropertyEditorSupport; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.Test; @@ -34,8 +39,6 @@ import org.springframework.web.bind.ServletRequestParameterPropertyValues; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.multipart.support.StringMultipartFileEditor; -import static org.junit.Assert.*; - /** * @author Juergen Hoeller */ @@ -126,6 +129,31 @@ public class WebRequestDataBinderTests { assertFalse(target.isPostProcessed()); } + // SPR-13502 + @Test + public void testCollectionFieldsDefault() throws Exception { + TestBean target = new TestBean(); + target.setSomeSet(null); + target.setSomeList(null); + target.setSomeMap(null); + WebRequestDataBinder binder = new WebRequestDataBinder(target); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("_someSet", "visible"); + request.addParameter("_someList", "visible"); + request.addParameter("_someMap", "visible"); + + binder.bind(new ServletWebRequest(request)); + assertThat(target.getSomeSet(), notNullValue()); + assertThat(target.getSomeSet(), isA(Set.class)); + + assertThat(target.getSomeList(), notNullValue()); + assertThat(target.getSomeList(), isA(List.class)); + + assertThat(target.getSomeMap(), notNullValue()); + assertThat(target.getSomeMap(), isA(Map.class)); + } + @Test public void testFieldDefaultPreemptsFieldMarker() throws Exception { TestBean target = new TestBean(); diff --git a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java index 6210cec1..258a9cc4 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package org.springframework.web.client; +import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.util.Collections; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -25,6 +28,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.junit.Assert; import org.junit.Test; import org.springframework.core.io.ClassPathResource; @@ -32,16 +36,28 @@ import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.AsyncClientHttpRequestExecution; +import org.springframework.http.client.AsyncClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory; +import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureCallback; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author Arjen Poutsma @@ -49,14 +65,14 @@ import static org.junit.Assert.*; */ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCase { - private final AsyncRestTemplate template = new AsyncRestTemplate(new HttpComponentsAsyncClientHttpRequestFactory()); + private final AsyncRestTemplate template = new AsyncRestTemplate( + new HttpComponentsAsyncClientHttpRequestFactory()); @Test public void getEntity() throws Exception { - Future<ResponseEntity<String>> futureEntity = - template.getForEntity(baseUrl + "/{method}", String.class, "get"); - ResponseEntity<String> entity = futureEntity.get(); + Future<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get"); + ResponseEntity<String> entity = future.get(); assertEquals("Invalid content", helloWorld, entity.getBody()); assertFalse("No headers", entity.getHeaders().isEmpty()); assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType()); @@ -65,10 +81,9 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa @Test public void multipleFutureGets() throws Exception { - Future<ResponseEntity<String>> futureEntity = - template.getForEntity(baseUrl + "/{method}", String.class, "get"); - futureEntity.get(); - futureEntity.get(); + Future<ResponseEntity<String>> future = template.getForEntity(baseUrl + "/{method}", String.class, "get"); + future.get(); + future.get(); } @Test @@ -88,9 +103,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - // wait till done - while (!futureEntity.isDone()) { - } + waitTillDone(futureEntity); } @Test @@ -103,9 +116,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType()); assertEquals("Invalid status code", HttpStatus.OK, entity.getStatusCode()); }, ex -> fail(ex.getMessage())); - // wait till done - while (!futureEntity.isDone()) { - } + waitTillDone(futureEntity); } @Test @@ -160,8 +171,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!headersFuture.isDone()) { - } + waitTillDone(headersFuture); } @Test @@ -169,15 +179,14 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa ListenableFuture<HttpHeaders> headersFuture = template.headForHeaders(baseUrl + "/get"); headersFuture.addCallback(result -> assertTrue("No Content-Type header", result.containsKey("Content-Type")), ex -> fail(ex.getMessage())); - while (!headersFuture.isDone()) { - } + waitTillDone(headersFuture); } @Test public void postForLocation() throws Exception { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15"))); - HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders); + HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders); Future<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post"); URI location = locationFuture.get(); assertEquals("Invalid location", new URI(baseUrl + "/post/1"), location); @@ -187,7 +196,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa public void postForLocationCallback() throws Exception { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15"))); - HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders); + HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders); final URI expected = new URI(baseUrl + "/post/1"); ListenableFuture<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post"); locationFuture.addCallback(new ListenableFutureCallback<URI>() { @@ -200,21 +209,19 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!locationFuture.isDone()) { - } + waitTillDone(locationFuture); } @Test public void postForLocationCallbackWithLambdas() throws Exception { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15"))); - HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders); + HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders); final URI expected = new URI(baseUrl + "/post/1"); ListenableFuture<URI> locationFuture = template.postForLocation(baseUrl + "/{method}", entity, "post"); locationFuture.addCallback(result -> assertEquals("Invalid location", expected, result), ex -> fail(ex.getMessage())); - while (!locationFuture.isDone()) { - } + waitTillDone(locationFuture); } @Test @@ -241,8 +248,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!responseEntityFuture.isDone()) { - } + waitTillDone(responseEntityFuture); } @Test @@ -250,10 +256,10 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld); ListenableFuture<ResponseEntity<String>> responseEntityFuture = template.postForEntity(baseUrl + "/{method}", requestEntity, String.class, "post"); - responseEntityFuture.addCallback(result -> assertEquals("Invalid content", helloWorld, result.getBody()), + responseEntityFuture.addCallback( + result -> assertEquals("Invalid content", helloWorld, result.getBody()), ex -> fail(ex.getMessage())); - while (!responseEntityFuture.isDone()) { - } + waitTillDone(responseEntityFuture); } @Test @@ -277,8 +283,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!responseEntityFuture.isDone()) { - } + waitTillDone(responseEntityFuture); } @Test @@ -300,16 +305,14 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!deletedFuture.isDone()) { - } + waitTillDone(deletedFuture); } @Test public void deleteCallbackWithLambdas() throws Exception { ListenableFuture<?> deletedFuture = template.delete(new URI(baseUrl + "/delete")); - deletedFuture.addCallback(result -> assertNull(result), ex -> fail(ex.getMessage())); - while (!deletedFuture.isDone()) { - } + deletedFuture.addCallback(Assert::assertNull, ex -> fail(ex.getMessage())); + waitTillDone(deletedFuture); } @Test @@ -377,8 +380,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa assertNotNull(ex.getResponseBodyAsString()); } }); - while (!future.isDone()) { - } + waitTillDone(future); } @Test @@ -391,8 +393,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa assertNotNull(hcex.getStatusText()); assertNotNull(hcex.getResponseBodyAsString()); }); - while (!future.isDone()) { - } + waitTillDone(future); } @Test @@ -429,8 +430,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa assertNotNull(hsex.getResponseBodyAsString()); } }); - while (!future.isDone()) { - } + waitTillDone(future); } @Test @@ -443,8 +443,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa assertNotNull(hsex.getStatusText()); assertNotNull(hsex.getResponseBodyAsString()); }); - while (!future.isDone()) { - } + waitTillDone(future); } @Test @@ -469,8 +468,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!allowedFuture.isDone()) { - } + waitTillDone(allowedFuture); } @Test @@ -479,8 +477,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa allowedFuture.addCallback(result -> assertEquals("Invalid response", EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD,HttpMethod.TRACE), result), ex -> fail(ex.getMessage())); - while (!allowedFuture.isDone()) { - } + waitTillDone(allowedFuture); } @Test @@ -513,8 +510,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!responseFuture.isDone()) { - } + waitTillDone(responseFuture); } @Test @@ -527,8 +523,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, String.class, "get"); responseFuture.addCallback(result -> assertEquals("Invalid content", helloWorld, result.getBody()), ex -> fail(ex.getMessage())); - while (!responseFuture.isDone()) { - } + waitTillDone(responseFuture); } @Test @@ -536,7 +531,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("MyHeader", "MyValue"); requestHeaders.setContentType(MediaType.TEXT_PLAIN); - HttpEntity<String> requestEntity = new HttpEntity<String>(helloWorld, requestHeaders); + HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders); Future<ResponseEntity<Void>> resultFuture = template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post"); ResponseEntity<Void> result = resultFuture.get(); @@ -550,7 +545,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("MyHeader", "MyValue"); requestHeaders.setContentType(MediaType.TEXT_PLAIN); - HttpEntity<String> requestEntity = new HttpEntity<String>(helloWorld, requestHeaders); + HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders); ListenableFuture<ResponseEntity<Void>> resultFuture = template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post"); final URI expected =new URI(baseUrl + "/post/1"); @@ -565,8 +560,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa fail(ex.getMessage()); } }); - while (!resultFuture.isDone()) { - } + waitTillDone(resultFuture); } @Test @@ -574,7 +568,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("MyHeader", "MyValue"); requestHeaders.setContentType(MediaType.TEXT_PLAIN); - HttpEntity<String> requestEntity = new HttpEntity<String>(helloWorld, requestHeaders); + HttpEntity<String> requestEntity = new HttpEntity<>(helloWorld, requestHeaders); ListenableFuture<ResponseEntity<Void>> resultFuture = template.exchange(baseUrl + "/{method}", HttpMethod.POST, requestEntity, Void.class, "post"); final URI expected =new URI(baseUrl + "/post/1"); @@ -582,13 +576,12 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa assertEquals("Invalid location", expected, result.getHeaders().getLocation()); assertFalse(result.hasBody()); }, ex -> fail(ex.getMessage())); - while (!resultFuture.isDone()) { - } + waitTillDone(resultFuture); } @Test public void multipart() throws Exception { - MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>(); + MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); parts.add("name 1", "value 1"); parts.add("name 2", "value 2+1"); parts.add("name 2", "value 2+2"); @@ -600,4 +593,73 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa future.get(); } + @Test + public void getAndInterceptResponse() throws Exception { + RequestInterceptor interceptor = new RequestInterceptor(); + template.setInterceptors(Collections.singletonList(interceptor)); + ListenableFuture<ResponseEntity<String>> future = template.getForEntity("/get", String.class); + + interceptor.latch.await(5, TimeUnit.SECONDS); + assertNotNull(interceptor.response); + assertEquals(HttpStatus.OK, interceptor.response.getStatusCode()); + assertNull(interceptor.exception); + assertEquals(helloWorld, future.get().getBody()); + } + + @Test + public void getAndInterceptError() throws Exception { + RequestInterceptor interceptor = new RequestInterceptor(); + template.setInterceptors(Collections.singletonList(interceptor)); + template.getForEntity("/status/notfound", String.class); + + interceptor.latch.await(5, TimeUnit.SECONDS); + assertNotNull(interceptor.response); + assertEquals(HttpStatus.NOT_FOUND, interceptor.response.getStatusCode()); + assertNull(interceptor.exception); + } + + private void waitTillDone(ListenableFuture<?> future) { + while (!future.isDone()) { + } + } + + + private static class RequestInterceptor implements AsyncClientHttpRequestInterceptor { + + private final CountDownLatch latch = new CountDownLatch(1); + + private volatile ClientHttpResponse response; + + private volatile Throwable exception; + + @Override + public ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body, + AsyncClientHttpRequestExecution execution) throws IOException { + + request = new HttpRequestWrapper(request) { + + @Override + public URI getURI() { + try { + return new URI(baseUrl + super.getURI().toString()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + }; + + ListenableFuture<ClientHttpResponse> future = execution.executeAsync(request, body); + future.addCallback( + resp -> { + response = resp; + this.latch.countDown(); + }, + ex -> { + exception = ex; + this.latch.countDown(); + }); + return future; + } + } } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index 149340b6..d5f00fc2 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,16 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.EnumSet; +import java.util.List; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import org.junit.Test; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; @@ -32,6 +37,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJacksonValue; @@ -215,7 +221,7 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { bean.setWith2("with"); bean.setWithout("without"); HttpEntity<MySampleBean> entity = new HttpEntity<MySampleBean>(bean, entityHeaders); - String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post"); + String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class); assertTrue(s.contains("\"with1\":\"with\"")); assertTrue(s.contains("\"with2\":\"with\"")); assertTrue(s.contains("\"without\":\"without\"")); @@ -229,7 +235,7 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { MappingJacksonValue jacksonValue = new MappingJacksonValue(bean); jacksonValue.setSerializationView(MyJacksonView1.class); HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(jacksonValue, entityHeaders); - String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post"); + String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class); assertTrue(s.contains("\"with1\":\"with\"")); assertFalse(s.contains("\"with2\":\"with\"")); assertFalse(s.contains("\"without\":\"without\"")); @@ -243,6 +249,21 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { assertEquals("Invalid content", helloWorld, s); } + @Test // SPR-13154 + public void jsonPostForObjectWithJacksonTypeInfoList() throws URISyntaxException { + List<ParentClass> list = new ArrayList<>(); + list.add(new Foo("foo")); + list.add(new Bar("bar")); + ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<ParentClass>>() {}; + RequestEntity<List<ParentClass>> entity = RequestEntity + .post(new URI(baseUrl + "/jsonpost")) + .contentType(new MediaType("application", "json", Charset.forName("UTF-8"))) + .body(list, typeReference.getType()); + String content = template.exchange(entity, String.class).getBody(); + assertTrue(content.contains("\"type\":\"foo\"")); + assertTrue(content.contains("\"type\":\"bar\"")); + } + public interface MyJacksonView1 {}; public interface MyJacksonView2 {}; @@ -290,4 +311,47 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { } } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + public static class ParentClass { + + private String parentProperty; + + public ParentClass() { + } + + public ParentClass(String parentProperty) { + this.parentProperty = parentProperty; + } + + public String getParentProperty() { + return parentProperty; + } + + public void setParentProperty(String parentProperty) { + this.parentProperty = parentProperty; + } + } + + @JsonTypeName("foo") + public static class Foo extends ParentClass { + + public Foo() { + } + + public Foo(String parentProperty) { + super(parentProperty); + } + } + + @JsonTypeName("bar") + public static class Bar extends ParentClass { + + public Bar() { + } + + public Bar(String parentProperty) { + super(parentProperty); + } + } + } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 1ecfc48e..5674ad55 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -49,6 +49,7 @@ import static org.mockito.BDDMockito.*; /** * @author Arjen Poutsma + * @author Rossen Stoyanchev */ @SuppressWarnings("unchecked") public class RestTemplateTests { @@ -135,7 +136,7 @@ public class RestTemplateTests { given(response.getStatusCode()).willReturn(status); given(response.getStatusText()).willReturn(status.getReasonPhrase()); - Map<String, String> vars = new HashMap<String, String>(2); + Map<String, String> vars = new HashMap<>(2); vars.put("first", null); vars.put("last", "foo"); template.execute("http://example.com/{first}-{last}", HttpMethod.GET, null, null, vars); @@ -278,7 +279,7 @@ public class RestTemplateTests { given(response.getHeaders()).willReturn(new HttpHeaders()); given(response.getBody()).willReturn(null); - Map<String, String> uriVariables = new HashMap<String, String>(2); + Map<String, String> uriVariables = new HashMap<>(2); uriVariables.put("hotel", "1"); uriVariables.put("publicpath", "pics/logo.png"); uriVariables.put("scale", "150x150"); @@ -351,7 +352,7 @@ public class RestTemplateTests { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.setContentType(contentType); - HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders); + HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders); URI result = template.postForLocation("http://example.com", entity); assertEquals("Invalid POST result", expected, result); @@ -379,7 +380,7 @@ public class RestTemplateTests { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.set("MyHeader", "MyValue"); - HttpEntity<String> entity = new HttpEntity<String>(helloWorld, entityHeaders); + HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders); URI result = template.postForLocation("http://example.com", entity); assertEquals("Invalid POST result", expected, result); @@ -622,21 +623,27 @@ public class RestTemplateTests { verify(response).close(); } + // Issue: SPR-9325, SPR-13860 + @Test public void ioException() throws Exception { + String url = "http://example.com/resource?access_token=123"; + given(converter.canRead(String.class, null)).willReturn(true); MediaType mediaType = new MediaType("foo", "bar"); given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(mediaType)); - given(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).willReturn(request); + given(requestFactory.createRequest(new URI(url), HttpMethod.GET)).willReturn(request); given(request.getHeaders()).willReturn(new HttpHeaders()); - given(request.execute()).willThrow(new IOException()); + given(request.execute()).willThrow(new IOException("Socket failure")); try { - template.getForObject("http://example.com/resource", String.class); + template.getForObject(url, String.class); fail("RestClientException expected"); } catch (ResourceAccessException ex) { - // expected + assertEquals("I/O error on GET request for \"http://example.com/resource\": " + + "Socket failure; nested exception is java.io.IOException: Socket failure", + ex.getMessage()); } } @@ -669,7 +676,7 @@ public class RestTemplateTests { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.set("MyHeader", "MyValue"); - HttpEntity<String> requestEntity = new HttpEntity<String>(body, entityHeaders); + HttpEntity<String> requestEntity = new HttpEntity<>(body, entityHeaders); ResponseEntity<Integer> result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, Integer.class); assertEquals("Invalid POST result", expected, result.getBody()); assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); @@ -692,9 +699,9 @@ public class RestTemplateTests { given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).willReturn(this.request); HttpHeaders requestHeaders = new HttpHeaders(); given(this.request.getHeaders()).willReturn(requestHeaders); - given(converter.canWrite(String.class, null)).willReturn(true); + given(converter.canWrite(String.class, String.class, null)).willReturn(true); String requestBody = "Hello World"; - converter.write(requestBody, null, this.request); + converter.write(requestBody, String.class, null, this.request); given(this.request.execute()).willReturn(response); given(errorHandler.hasError(response)).willReturn(false); List<Integer> expected = Collections.singletonList(42); @@ -703,7 +710,7 @@ public class RestTemplateTests { responseHeaders.setContentLength(10); given(response.getStatusCode()).willReturn(HttpStatus.OK); given(response.getHeaders()).willReturn(responseHeaders); - given(response.getBody()).willReturn(new ByteArrayInputStream(new Integer(42).toString().getBytes())); + given(response.getBody()).willReturn(new ByteArrayInputStream(Integer.toString(42).getBytes())); given(converter.canRead(intList.getType(), null, MediaType.TEXT_PLAIN)).willReturn(true); given(converter.read(eq(intList.getType()), eq(null), any(HttpInputMessage.class))).willReturn(expected); given(response.getStatusCode()).willReturn(HttpStatus.OK); @@ -713,7 +720,7 @@ public class RestTemplateTests { HttpHeaders entityHeaders = new HttpHeaders(); entityHeaders.set("MyHeader", "MyValue"); - HttpEntity<String> requestEntity = new HttpEntity<String>(requestBody, entityHeaders); + HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, entityHeaders); ResponseEntity<List<Integer>> result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, intList); assertEquals("Invalid POST result", expected, result.getBody()); assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java index ef1afaad..603e2642 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletRequestAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,8 +78,7 @@ public class ServletRequestAttributesTests { request.setSession(session); ServletRequestAttributes attrs = new ServletRequestAttributes(request); attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_SESSION); - Object value = session.getAttribute(KEY); - assertSame(VALUE, value); + assertSame(VALUE, session.getAttribute(KEY)); } @Test @@ -89,11 +88,11 @@ public class ServletRequestAttributesTests { MockHttpServletRequest request = new MockHttpServletRequest(); request.setSession(session); ServletRequestAttributes attrs = new ServletRequestAttributes(request); + assertSame(VALUE, attrs.getAttribute(KEY, RequestAttributes.SCOPE_SESSION)); attrs.requestCompleted(); request.close(); attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_SESSION); - Object value = session.getAttribute(KEY); - assertSame(VALUE, value); + assertSame(VALUE, session.getAttribute(KEY)); } @Test @@ -104,8 +103,7 @@ public class ServletRequestAttributesTests { request.setSession(session); ServletRequestAttributes attrs = new ServletRequestAttributes(request); attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_GLOBAL_SESSION); - Object value = session.getAttribute(KEY); - assertSame(VALUE, value); + assertSame(VALUE, session.getAttribute(KEY)); } @Test @@ -115,11 +113,11 @@ public class ServletRequestAttributesTests { MockHttpServletRequest request = new MockHttpServletRequest(); request.setSession(session); ServletRequestAttributes attrs = new ServletRequestAttributes(request); + assertSame(VALUE, attrs.getAttribute(KEY, RequestAttributes.SCOPE_GLOBAL_SESSION)); attrs.requestCompleted(); request.close(); attrs.setAttribute(KEY, VALUE, RequestAttributes.SCOPE_GLOBAL_SESSION); - Object value = session.getAttribute(KEY); - assertSame(VALUE, value); + assertSame(VALUE, session.getAttribute(KEY)); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java index 3832ae3a..a5a8b07c 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.web.context.request; +import static org.junit.Assert.*; + import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; @@ -32,8 +34,6 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; -import static org.junit.Assert.*; - /** * Parameterized tests for ServletWebRequest * @author Juergen Hoeller @@ -145,6 +145,18 @@ public class ServletWebRequestHttpMethodsTests { } @Test + public void checkNotModifiedETagWithSeparatorChars() { + String eTag = "\"Foo, Bar\""; + servletRequest.addHeader("If-None-Match", eTag); + + assertTrue(request.checkNotModified(eTag)); + + assertEquals(304, servletResponse.getStatus()); + assertEquals(eTag, servletResponse.getHeader("ETag")); + } + + + @Test public void checkModifiedETag() { String currentETag = "\"Foo\""; String oldEtag = "Bar"; @@ -204,6 +216,7 @@ public class ServletWebRequestHttpMethodsTests { assertEquals(dateFormat.format(currentDate.getTime()), servletResponse.getHeader("Last-Modified")); } + // SPR-14224 @Test public void checkNotModifiedETagAndModifiedTimestamp() { String eTag = "\"Foo\""; @@ -212,9 +225,9 @@ public class ServletWebRequestHttpMethodsTests { long oneMinuteAgo = currentEpoch - (1000 * 60); servletRequest.addHeader("If-Modified-Since", oneMinuteAgo); - assertFalse(request.checkNotModified(eTag, currentEpoch)); + assertTrue(request.checkNotModified(eTag, currentEpoch)); - assertEquals(200, servletResponse.getStatus()); + assertEquals(304, servletResponse.getStatus()); assertEquals(eTag, servletResponse.getHeader("ETag")); assertEquals(dateFormat.format(currentEpoch), servletResponse.getHeader("Last-Modified")); } @@ -293,4 +306,28 @@ public class ServletWebRequestHttpMethodsTests { assertEquals(dateFormat.format(epochTime), servletResponse.getHeader("Last-Modified")); } + @Test + public void checkNotModifiedTimestampConditionalPut() throws Exception { + long currentEpoch = currentDate.getTime(); + long oneMinuteAgo = currentEpoch - (1000 * 60); + servletRequest.setMethod("PUT"); + servletRequest.addHeader("If-UnModified-Since", currentEpoch); + + assertFalse(request.checkNotModified(oneMinuteAgo)); + assertEquals(200, servletResponse.getStatus()); + assertEquals(null, servletResponse.getHeader("Last-Modified")); + } + + @Test + public void checkNotModifiedTimestampConditionalPutConflict() throws Exception { + long currentEpoch = currentDate.getTime(); + long oneMinuteAgo = currentEpoch - (1000 * 60); + servletRequest.setMethod("PUT"); + servletRequest.addHeader("If-UnModified-Since", oneMinuteAgo); + + assertTrue(request.checkNotModified(currentEpoch)); + assertEquals(412, servletResponse.getStatus()); + assertEquals(null, servletResponse.getHeader("Last-Modified")); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java b/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java index ab198238..a44318fc 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/SessionScopeTests.java @@ -172,6 +172,11 @@ public class SessionScopeTests { @Override public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { } + + @Override + public boolean requiresDestruction(Object bean) { + return true; + } } @@ -195,6 +200,11 @@ public class SessionScopeTests { ((BeanNameAware) bean).setBeanName(null); } } + + @Override + public boolean requiresDestruction(Object bean) { + return true; + } } } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java index 50d84230..2596796e 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java @@ -147,7 +147,7 @@ public class StandardServletAsyncWebRequestTests { } // SPR-13292 - + @Test public void onCompletionHandlerAfterOnErrorEvent() throws Exception { Runnable handler = mock(Runnable.class); diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java index c763eeec..d4a2ad47 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTests.java @@ -150,8 +150,9 @@ public class WebAsyncManagerTests { try { this.asyncManager.startCallableProcessing(task); fail("Expected Exception"); - }catch(Exception e) { - assertEquals(exception, e); + } + catch (Exception ex) { + assertEquals(exception, ex); } assertFalse(this.asyncManager.hasConcurrentResult()); @@ -162,7 +163,6 @@ public class WebAsyncManagerTests { @Test public void startCallableProcessingPreProcessException() throws Exception { - Callable<Object> task = new StubCallable(21); Exception exception = new Exception(); @@ -183,7 +183,6 @@ public class WebAsyncManagerTests { @Test public void startCallableProcessingPostProcessException() throws Exception { - Callable<Object> task = new StubCallable(21); Exception exception = new Exception(); @@ -205,7 +204,6 @@ public class WebAsyncManagerTests { @Test public void startCallableProcessingPostProcessContinueAfterException() throws Exception { - Callable<Object> task = new StubCallable(21); Exception exception = new Exception(); @@ -231,7 +229,6 @@ public class WebAsyncManagerTests { @Test public void startCallableProcessingWithAsyncTask() throws Exception { - AsyncTaskExecutor executor = mock(AsyncTaskExecutor.class); given(this.asyncWebRequest.getNativeRequest(HttpServletRequest.class)).willReturn(this.servletRequest); @@ -259,7 +256,6 @@ public class WebAsyncManagerTests { @Test public void startDeferredResultProcessing() throws Exception { - DeferredResult<String> deferredResult = new DeferredResult<String>(1000L); String concurrentResult = "abc"; @@ -282,7 +278,6 @@ public class WebAsyncManagerTests { @Test public void startDeferredResultProcessingBeforeConcurrentHandlingException() throws Exception { - DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); Exception exception = new Exception(); @@ -295,7 +290,7 @@ public class WebAsyncManagerTests { this.asyncManager.startDeferredResultProcessing(deferredResult); fail("Expected Exception"); } - catch(Exception success) { + catch (Exception success) { assertEquals(exception, success); } @@ -328,7 +323,6 @@ public class WebAsyncManagerTests { @Test public void startDeferredResultProcessingPostProcessException() throws Exception { - DeferredResult<Integer> deferredResult = new DeferredResult<Integer>(); Exception exception = new Exception(); @@ -371,6 +365,7 @@ public class WebAsyncManagerTests { verify(this.asyncWebRequest).dispatch(); } + private final class StubCallable implements Callable<Object> { private Object value; @@ -388,6 +383,7 @@ public class WebAsyncManagerTests { } } + @SuppressWarnings("serial") private static class SyncTaskExecutor extends SimpleAsyncTaskExecutor { diff --git a/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java b/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java index a712e525..dd56ff00 100644 --- a/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java +++ b/spring-web/src/test/java/org/springframework/web/context/support/Spr8510Tests.java @@ -49,7 +49,8 @@ public class Spr8510Tests { try { cll.contextInitialized(new ServletContextEvent(sc)); fail("expected exception"); - } catch (Throwable t) { + } + catch (Throwable t) { // assert that an attempt was made to load the correct XML assertTrue(t.getMessage(), t.getMessage().endsWith( "Could not open ServletContext resource [/programmatic.xml]")); @@ -75,7 +76,8 @@ public class Spr8510Tests { try { cll.contextInitialized(new ServletContextEvent(sc)); fail("expected exception"); - } catch (Throwable t) { + } + catch (Throwable t) { // assert that an attempt was made to load the correct XML assertTrue(t.getMessage(), t.getMessage().endsWith( "Could not open ServletContext resource [/from-init-param.xml]")); @@ -98,7 +100,8 @@ public class Spr8510Tests { try { cll.contextInitialized(new ServletContextEvent(sc)); fail("expected exception"); - } catch (Throwable t) { + } + catch (Throwable t) { // assert that an attempt was made to load the correct XML assertTrue(t.getMessage().endsWith( "Could not open ServletContext resource [/from-init-param.xml]")); @@ -125,7 +128,8 @@ public class Spr8510Tests { try { cll.contextInitialized(new ServletContextEvent(sc)); fail("expected exception"); - } catch (Throwable t) { + } + catch (Throwable t) { // assert that an attempt was made to load the correct XML System.out.println(t.getMessage()); assertTrue(t.getMessage().endsWith( @@ -150,7 +154,8 @@ public class Spr8510Tests { try { cll.contextInitialized(new ServletContextEvent(sc)); fail("expected exception"); - } catch (Throwable t) { + } + catch (Throwable t) { // assert that an attempt was made to load the correct XML System.out.println(t.getMessage()); assertTrue(t.getMessage().endsWith( diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 8d3651c5..32f68af3 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -176,7 +176,7 @@ public class CorsConfigurationTests { @Test public void checkMethodAllowed() { - assertEquals(Arrays.asList(HttpMethod.GET), config.checkHttpMethod(HttpMethod.GET)); + assertEquals(Arrays.asList(HttpMethod.GET, HttpMethod.HEAD), config.checkHttpMethod(HttpMethod.GET)); config.addAllowedMethod("GET"); assertEquals(Arrays.asList(HttpMethod.GET), config.checkHttpMethod(HttpMethod.GET)); config.addAllowedMethod("POST"); @@ -189,7 +189,7 @@ public class CorsConfigurationTests { assertNull(config.checkHttpMethod(null)); assertNull(config.checkHttpMethod(HttpMethod.DELETE)); config.setAllowedMethods(new ArrayList<>()); - assertNull(config.checkHttpMethod(HttpMethod.HEAD)); + assertNull(config.checkHttpMethod(HttpMethod.POST)); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 56ab6166..30a93e30 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -171,7 +171,7 @@ public class DefaultCorsProcessorTests { this.conf.addAllowedOrigin("*"); this.processor.processRequest(this.conf, request, response); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); - assertEquals("GET", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); + assertEquals("GET,HEAD", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)); } @Test diff --git a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java index 64392f91..5f0006c9 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -145,4 +145,26 @@ public class CharacterEncodingFilterTests { verify(filterChain).doFilter(request, response); } + // SPR-14240 + @Test + public void setForceEncodingOnRequestOnly() throws Exception { + HttpServletRequest request = mock(HttpServletRequest.class); + request.setCharacterEncoding(ENCODING); + given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); + given(request.getAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX)).willReturn(null); + + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + CharacterEncodingFilter filter = new CharacterEncodingFilter(ENCODING, true, false); + filter.init(new MockFilterConfig(FILTER_NAME)); + filter.doFilter(request, response, filterChain); + + verify(request).setAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX, Boolean.TRUE); + verify(request).removeAttribute(FILTER_NAME + OncePerRequestFilter.ALREADY_FILTERED_SUFFIX); + verify(request, times(2)).setCharacterEncoding(ENCODING); + verify(response, never()).setCharacterEncoding(ENCODING); + verify(filterChain).doFilter(request, response); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java new file mode 100644 index 00000000..56d93f8c --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.filter; + +import java.io.IOException; +import java.util.Enumeration; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.test.MockFilterChain; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link ForwardedHeaderFilter}. + * @author Rossen Stoyanchev + * @author Eddú Meléndez + */ +public class ForwardedHeaderFilterTests { + + private static final String X_FORWARDED_PROTO = "x-forwarded-proto"; // SPR-14372 (case insensitive) + private static final String X_FORWARDED_HOST = "x-forwarded-host"; + private static final String X_FORWARDED_PORT = "x-forwarded-port"; + private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix"; + + + private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); + + private MockHttpServletRequest request; + + private MockFilterChain filterChain; + + + @Before + @SuppressWarnings("serial") + public void setUp() throws Exception { + this.request = new MockHttpServletRequest(); + this.request.setScheme("http"); + this.request.setServerName("localhost"); + this.request.setServerPort(80); + this.filterChain = new MockFilterChain(new HttpServlet() {}); + } + + + @Test + public void contextPathEmpty() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, ""); + assertEquals("", filterAndGetContextPath()); + } + + @Test + public void contextPathWithTrailingSlash() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/foo/bar/"); + assertEquals("/foo/bar", filterAndGetContextPath()); + } + + @Test + public void contextPathWithTrailingSlashes() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/foo/bar/baz///"); + assertEquals("/foo/bar/baz", filterAndGetContextPath()); + } + + @Test + public void requestUri() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/"); + this.request.setContextPath("/app"); + this.request.setRequestURI("/app/path"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertEquals("", actual.getContextPath()); + assertEquals("/path", actual.getRequestURI()); + } + + @Test + public void requestUriWithTrailingSlash() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/"); + this.request.setContextPath("/app"); + this.request.setRequestURI("/app/path/"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertEquals("", actual.getContextPath()); + assertEquals("/path/", actual.getRequestURI()); + } + @Test + public void requestUriEqualsContextPath() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/"); + this.request.setContextPath("/app"); + this.request.setRequestURI("/app"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertEquals("", actual.getContextPath()); + assertEquals("/", actual.getRequestURI()); + } + + @Test + public void requestUriRootUrl() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/"); + this.request.setContextPath("/app"); + this.request.setRequestURI("/app/"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertEquals("", actual.getContextPath()); + assertEquals("/", actual.getRequestURI()); + } + + @Test + public void caseInsensitiveForwardedPrefix() throws Exception { + this.request = new MockHttpServletRequest() { + + // Make it case-sensitive (SPR-14372) + + @Override + public String getHeader(String header) { + Enumeration<String> names = getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if (name.equals(header)) { + return super.getHeader(header); + } + } + return null; + } + }; + this.request.addHeader(X_FORWARDED_PREFIX, "/prefix"); + this.request.setRequestURI("/path"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertEquals("/prefix/path", actual.getRequestURI()); + } + + @Test + public void shouldFilter() throws Exception { + testShouldFilter("Forwarded"); + testShouldFilter(X_FORWARDED_HOST); + testShouldFilter(X_FORWARDED_PORT); + testShouldFilter(X_FORWARDED_PROTO); + } + + @Test + public void shouldNotFilter() throws Exception { + assertTrue(this.filter.shouldNotFilter(new MockHttpServletRequest())); + } + + @Test + public void forwardedRequest() throws Exception { + this.request.setRequestURI("/mvc-showcase"); + this.request.addHeader(X_FORWARDED_PROTO, "https"); + this.request.addHeader(X_FORWARDED_HOST, "84.198.58.199"); + this.request.addHeader(X_FORWARDED_PORT, "443"); + this.request.addHeader("foo", "bar"); + + this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain); + HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest(); + + assertEquals("https://84.198.58.199/mvc-showcase", actual.getRequestURL().toString()); + assertEquals("https", actual.getScheme()); + assertEquals("84.198.58.199", actual.getServerName()); + assertEquals(443, actual.getServerPort()); + assertTrue(actual.isSecure()); + + assertNull(actual.getHeader(X_FORWARDED_PROTO)); + assertNull(actual.getHeader(X_FORWARDED_HOST)); + assertNull(actual.getHeader(X_FORWARDED_PORT)); + assertEquals("bar", actual.getHeader("foo")); + } + + @Test + public void requestUriWithForwardedPrefix() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/prefix"); + this.request.setRequestURI("/mvc-showcase"); + + HttpServletRequest actual = filterAndGetWrappedRequest(); + assertEquals("http://localhost/prefix/mvc-showcase", actual.getRequestURL().toString()); + } + + @Test + public void requestUriWithForwardedPrefixTrailingSlash() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/prefix/"); + this.request.setRequestURI("/mvc-showcase"); + + HttpServletRequest actual = filterAndGetWrappedRequest(); + assertEquals("http://localhost/prefix/mvc-showcase", actual.getRequestURL().toString()); + } + + @Test + public void contextPathWithForwardedPrefix() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/prefix"); + this.request.setContextPath("/mvc-showcase"); + + String actual = filterAndGetContextPath(); + assertEquals("/prefix", actual); + } + + @Test + public void contextPathWithForwardedPrefixTrailingSlash() throws Exception { + this.request.addHeader(X_FORWARDED_PREFIX, "/prefix/"); + this.request.setContextPath("/mvc-showcase"); + + String actual = filterAndGetContextPath(); + assertEquals("/prefix", actual); + } + + private String filterAndGetContextPath() throws ServletException, IOException { + return filterAndGetWrappedRequest().getContextPath(); + } + + private HttpServletRequest filterAndGetWrappedRequest() throws ServletException, IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + this.filter.doFilterInternal(this.request, response, this.filterChain); + return (HttpServletRequest) this.filterChain.getRequest(); + } + + private void testShouldFilter(String headerName) throws ServletException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(headerName, "1"); + assertFalse(this.filter.shouldNotFilter(request)); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index 0c5af485..893501b2 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,6 +74,26 @@ public class ShallowEtagHeaderFilterTests { } @Test + public void filterNoMatchWeakETag() throws Exception { + this.filter.setWriteWeakETag(true); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final byte[] responseBody = "Hello World".getBytes("UTF-8"); + FilterChain filterChain = (filterRequest, filterResponse) -> { + assertEquals("Invalid request passed", request, filterRequest); + ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); + }; + filter.doFilter(request, response, filterChain); + + assertEquals("Invalid status", 200, response.getStatus()); + assertEquals("Invalid ETag header", "W/\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag")); + assertTrue("Invalid Content-Length header", response.getContentLength() > 0); + assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray()); + } + + @Test public void filterMatch() throws Exception { final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; @@ -95,6 +115,27 @@ public class ShallowEtagHeaderFilterTests { } @Test + public void filterMatchWeakEtag() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; + request.addHeader("If-None-Match", "W/" + etag); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = (filterRequest, filterResponse) -> { + assertEquals("Invalid request passed", request, filterRequest); + byte[] responseBody = "Hello World".getBytes("UTF-8"); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); + filterResponse.setContentLength(responseBody.length); + }; + filter.doFilter(request, response, filterChain); + + assertEquals("Invalid status", 304, response.getStatus()); + assertEquals("Invalid ETag header", "\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag")); + assertFalse("Response has Content-Length header", response.containsHeader("Content-Length")); + assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray()); + } + + @Test public void filterWriter() throws Exception { final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java index 21fc00e8..2010ba9d 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java @@ -24,6 +24,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.tests.sample.beans.TestBean; import org.springframework.validation.BindException; @@ -51,56 +52,56 @@ import static org.mockito.BDDMockito.*; */ public class ModelAttributeMethodProcessorTests { + private NativeWebRequest request; + + private ModelAndViewContainer container; + private ModelAttributeMethodProcessor processor; private MethodParameter paramNamedValidModelAttr; - private MethodParameter paramErrors; - private MethodParameter paramInt; - private MethodParameter paramModelAttr; - + private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; private MethodParameter returnParamNamedModelAttr; - private MethodParameter returnParamNonSimpleType; - private ModelAndViewContainer mavContainer; - - private NativeWebRequest webRequest; @Before public void setUp() throws Exception { - processor = new ModelAttributeMethodProcessor(false); + this.request = new ServletWebRequest(new MockHttpServletRequest()); + this.container = new ModelAndViewContainer(); + this.processor = new ModelAttributeMethodProcessor(false); Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", - TestBean.class, Errors.class, int.class, TestBean.class, TestBean.class); + TestBean.class, Errors.class, int.class, TestBean.class, + TestBean.class, TestBean.class); - paramNamedValidModelAttr = new MethodParameter(method, 0); - paramErrors = new MethodParameter(method, 1); - paramInt = new MethodParameter(method, 2); - paramModelAttr = new MethodParameter(method, 3); - paramNonSimpleType = new MethodParameter(method, 4); + this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); + this.paramErrors = new SynthesizingMethodParameter(method, 1); + this.paramInt = new SynthesizingMethodParameter(method, 2); + this.paramModelAttr = new SynthesizingMethodParameter(method, 3); + this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); + this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); - returnParamNamedModelAttr = new MethodParameter(getClass().getDeclaredMethod("annotatedReturnValue"), -1); - returnParamNonSimpleType = new MethodParameter(getClass().getDeclaredMethod("notAnnotatedReturnValue"), -1); + method = getClass().getDeclaredMethod("annotatedReturnValue"); + this.returnParamNamedModelAttr = new MethodParameter(method, -1); - mavContainer = new ModelAndViewContainer(); - - webRequest = new ServletWebRequest(new MockHttpServletRequest()); + method = getClass().getDeclaredMethod("notAnnotatedReturnValue"); + this.returnParamNonSimpleType = new MethodParameter(method, -1); } + @Test public void supportedParameters() throws Exception { - // Only @ModelAttribute arguments - assertTrue(processor.supportsParameter(paramNamedValidModelAttr)); - assertTrue(processor.supportsParameter(paramModelAttr)); + assertTrue(this.processor.supportsParameter(this.paramNamedValidModelAttr)); + assertTrue(this.processor.supportsParameter(this.paramModelAttr)); - assertFalse(processor.supportsParameter(paramErrors)); - assertFalse(processor.supportsParameter(paramInt)); - assertFalse(processor.supportsParameter(paramNonSimpleType)); + assertFalse(this.processor.supportsParameter(this.paramErrors)); + assertFalse(this.processor.supportsParameter(this.paramInt)); + assertFalse(this.processor.supportsParameter(this.paramNonSimpleType)); } @Test @@ -108,135 +109,162 @@ public class ModelAttributeMethodProcessorTests { processor = new ModelAttributeMethodProcessor(true); // Only non-simple types, even if not annotated - assertTrue(processor.supportsParameter(paramNamedValidModelAttr)); - assertTrue(processor.supportsParameter(paramErrors)); - assertTrue(processor.supportsParameter(paramModelAttr)); - assertTrue(processor.supportsParameter(paramNonSimpleType)); + assertTrue(this.processor.supportsParameter(this.paramNamedValidModelAttr)); + assertTrue(this.processor.supportsParameter(this.paramErrors)); + assertTrue(this.processor.supportsParameter(this.paramModelAttr)); + assertTrue(this.processor.supportsParameter(this.paramNonSimpleType)); - assertFalse(processor.supportsParameter(paramInt)); + assertFalse(this.processor.supportsParameter(this.paramInt)); } @Test public void supportedReturnTypes() throws Exception { processor = new ModelAttributeMethodProcessor(false); - assertTrue(processor.supportsReturnType(returnParamNamedModelAttr)); - assertFalse(processor.supportsReturnType(returnParamNonSimpleType)); + assertTrue(this.processor.supportsReturnType(returnParamNamedModelAttr)); + assertFalse(this.processor.supportsReturnType(returnParamNonSimpleType)); } @Test public void supportedReturnTypesInDefaultResolutionMode() throws Exception { processor = new ModelAttributeMethodProcessor(true); - assertTrue(processor.supportsReturnType(returnParamNamedModelAttr)); - assertTrue(processor.supportsReturnType(returnParamNonSimpleType)); + assertTrue(this.processor.supportsReturnType(returnParamNamedModelAttr)); + assertTrue(this.processor.supportsReturnType(returnParamNonSimpleType)); } @Test public void bindExceptionRequired() throws Exception { - assertTrue(processor.isBindExceptionRequired(null, paramNonSimpleType)); + assertTrue(this.processor.isBindExceptionRequired(null, this.paramNonSimpleType)); + assertFalse(this.processor.isBindExceptionRequired(null, this.paramNamedValidModelAttr)); } @Test - public void bindExceptionNotRequired() throws Exception { - assertFalse(processor.isBindExceptionRequired(null, paramNamedValidModelAttr)); + public void resolveArgumentFromModel() throws Exception { + testGetAttributeFromModel("attrName", this.paramNamedValidModelAttr); + testGetAttributeFromModel("testBean", this.paramModelAttr); + testGetAttributeFromModel("testBean", this.paramNonSimpleType); } @Test - public void resovleArgumentFromModel() throws Exception { - getAttributeFromModel("attrName", paramNamedValidModelAttr); - getAttributeFromModel("testBean", paramModelAttr); - getAttributeFromModel("testBean", paramNonSimpleType); + public void resovleArgumentViaDefaultConstructor() throws Exception { + WebDataBinder dataBinder = new WebRequestDataBinder(null); + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(anyObject(), notNull(), eq("attrName"))).willReturn(dataBinder); + + this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); + verify(factory).createBinder(anyObject(), notNull(), eq("attrName")); } - private void getAttributeFromModel(String expectedAttributeName, MethodParameter param) throws Exception { + @Test + public void resolveArgumentValidation() throws Exception { + String name = "attrName"; Object target = new TestBean(); - mavContainer.addAttribute(expectedAttributeName, target); + this.container.addAttribute(name, target); - WebDataBinder dataBinder = new WebRequestDataBinder(target); + StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); - given(factory.createBinder(webRequest, target, expectedAttributeName)).willReturn(dataBinder); + given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); - processor.resolveArgument(param, mavContainer, webRequest, factory); - verify(factory).createBinder(webRequest, target, expectedAttributeName); + this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); + + assertTrue(dataBinder.isBindInvoked()); + assertTrue(dataBinder.isValidateInvoked()); } @Test - public void resovleArgumentViaDefaultConstructor() throws Exception { - WebDataBinder dataBinder = new WebRequestDataBinder(null); + public void resolveArgumentBindingDisabledPreviously() throws Exception { + String name = "attrName"; + Object target = new TestBean(); + this.container.addAttribute(name, target); + + // Declare binding disabled (e.g. via @ModelAttribute method) + this.container.setBindingDisabled(name); + StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); - given(factory.createBinder((NativeWebRequest) anyObject(), notNull(), eq("attrName"))).willReturn(dataBinder); + given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); - processor.resolveArgument(paramNamedValidModelAttr, mavContainer, webRequest, factory); + this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); - verify(factory).createBinder((NativeWebRequest) anyObject(), notNull(), eq("attrName")); + assertFalse(dataBinder.isBindInvoked()); + assertTrue(dataBinder.isValidateInvoked()); } @Test - public void resolveArgumentValidation() throws Exception { - String name = "attrName"; + public void resolveArgumentBindingDisabled() throws Exception { + String name = "noBindAttr"; Object target = new TestBean(); - mavContainer.addAttribute(name, target); + this.container.addAttribute(name, target); StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); - WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); - given(binderFactory.createBinder(webRequest, target, name)).willReturn(dataBinder); + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); - processor.resolveArgument(paramNamedValidModelAttr, mavContainer, webRequest, binderFactory); + this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory); - assertTrue(dataBinder.isBindInvoked()); + assertFalse(dataBinder.isBindInvoked()); assertTrue(dataBinder.isValidateInvoked()); } @Test(expected = BindException.class) - public void resovleArgumentBindException() throws Exception { + public void resolveArgumentBindException() throws Exception { String name = "testBean"; Object target = new TestBean(); - mavContainer.getModel().addAttribute(target); + this.container.getModel().addAttribute(target); StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); dataBinder.getBindingResult().reject("error"); - WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); - given(binderFactory.createBinder(webRequest, target, name)).willReturn(dataBinder); + given(binderFactory.createBinder(this.request, target, name)).willReturn(dataBinder); - processor.resolveArgument(paramNonSimpleType, mavContainer, webRequest, binderFactory); - verify(binderFactory).createBinder(webRequest, target, name); + this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory); + verify(binderFactory).createBinder(this.request, target, name); } @Test // SPR-9378 public void resolveArgumentOrdering() throws Exception { String name = "testBean"; Object testBean = new TestBean(name); - mavContainer.addAttribute(name, testBean); - mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, testBean); + this.container.addAttribute(name, testBean); + this.container.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, testBean); Object anotherTestBean = new TestBean(); - mavContainer.addAttribute("anotherTestBean", anotherTestBean); + this.container.addAttribute("anotherTestBean", anotherTestBean); StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name); WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); - given(binderFactory.createBinder(webRequest, testBean, name)).willReturn(dataBinder); + given(binderFactory.createBinder(this.request, testBean, name)).willReturn(dataBinder); - processor.resolveArgument(paramModelAttr, mavContainer, webRequest, binderFactory); + this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory); - assertSame("Resolved attribute should be updated to be last in the order", - testBean, mavContainer.getModel().values().toArray()[1]); - assertSame("BindingResult of resolved attribute should be last in the order", - dataBinder.getBindingResult(), mavContainer.getModel().values().toArray()[2]); + Object[] values = this.container.getModel().values().toArray(); + assertSame("Resolved attribute should be updated to be last", testBean, values[1]); + assertSame("BindingResult of resolved attr should be last", dataBinder.getBindingResult(), values[2]); } @Test public void handleAnnotatedReturnValue() throws Exception { - processor.handleReturnValue("expected", returnParamNamedModelAttr, mavContainer, webRequest); - assertEquals("expected", mavContainer.getModel().get("modelAttrName")); + this.processor.handleReturnValue("expected", this.returnParamNamedModelAttr, this.container, this.request); + assertEquals("expected", this.container.getModel().get("modelAttrName")); } @Test public void handleNotAnnotatedReturnValue() throws Exception { TestBean testBean = new TestBean("expected"); - processor.handleReturnValue(testBean, returnParamNonSimpleType, mavContainer, webRequest); + this.processor.handleReturnValue(testBean, this.returnParamNonSimpleType, this.container, this.request); + assertSame(testBean, this.container.getModel().get("testBean")); + } + - assertSame(testBean, mavContainer.getModel().get("testBean")); + private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { + Object target = new TestBean(); + this.container.addAttribute(expectedAttrName, target); + + WebDataBinder dataBinder = new WebRequestDataBinder(target); + WebDataBinderFactory factory = mock(WebDataBinderFactory.class); + given(factory.createBinder(this.request, target, expectedAttrName)).willReturn(dataBinder); + + this.processor.resolveArgument(param, this.container, this.request, factory); + verify(factory).createBinder(this.request, target, expectedAttrName); } @@ -246,6 +274,7 @@ public class ModelAttributeMethodProcessorTests { private boolean validateInvoked; + public StubRequestDataBinder(Object target, String objectName) { super(target, objectName); } @@ -285,13 +314,18 @@ public class ModelAttributeMethodProcessorTests { private static class ModelAttributeHandler { @SuppressWarnings("unused") - public void modelAttribute(@ModelAttribute("attrName") @Valid TestBean annotatedAttr, Errors errors, - int intArg, @ModelAttribute TestBean defaultNameAttr, TestBean notAnnotatedAttr) { + public void modelAttribute( + @ModelAttribute("attrName") @Valid TestBean annotatedAttr, + Errors errors, + int intArg, + @ModelAttribute TestBean defaultNameAttr, + @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, + TestBean notAnnotatedAttr) { } } - @ModelAttribute("modelAttrName") + @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { return null; } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java index e0a3c69f..24c61825 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,12 @@ package org.springframework.web.method.annotation; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; - import java.lang.reflect.Method; -import java.util.Arrays; +import java.util.Collections; import org.junit.Before; import org.junit.Test; + import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.ui.Model; @@ -43,10 +36,19 @@ import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; import org.springframework.web.method.support.InvocableHandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; + /** * Text fixture for {@link ModelFactory} tests. @@ -55,103 +57,116 @@ import org.springframework.web.method.support.ModelAndViewContainer; */ public class ModelFactoryTests { - private TestController controller = new TestController(); + private NativeWebRequest webRequest; - private InvocableHandlerMethod handleMethod; + private SessionAttributesHandler attributeHandler; - private InvocableHandlerMethod handleSessionAttrMethod; + private SessionAttributeStore attributeStore; - private SessionAttributesHandler sessionAttrsHandler; + private TestController controller = new TestController(); - private SessionAttributeStore sessionAttributeStore; - - private NativeWebRequest webRequest; + private ModelAndViewContainer mavContainer; @Before public void setUp() throws Exception { - this.controller = new TestController(); - - Method method = TestController.class.getDeclaredMethod("handle"); - this.handleMethod = new InvocableHandlerMethod(this.controller, method); - - method = TestController.class.getDeclaredMethod("handleSessionAttr", String.class); - this.handleSessionAttrMethod = new InvocableHandlerMethod(this.controller, method); - - this.sessionAttributeStore = new DefaultSessionAttributeStore(); - this.sessionAttrsHandler = new SessionAttributesHandler(TestController.class, this.sessionAttributeStore); this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + this.attributeStore = new DefaultSessionAttributeStore(); + this.attributeHandler = new SessionAttributesHandler(TestController.class, this.attributeStore); + this.controller = new TestController(); + this.mavContainer = new ModelAndViewContainer(); } @Test public void modelAttributeMethod() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttr", Model.class); - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); - assertEquals(Boolean.TRUE, mavContainer.getModel().get("modelAttr")); + assertEquals(Boolean.TRUE, this.mavContainer.getModel().get("modelAttr")); } @Test public void modelAttributeMethodWithExplicitName() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttrWithName"); - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); - assertEquals(Boolean.TRUE, mavContainer.getModel().get("name")); + assertEquals(Boolean.TRUE, this.mavContainer.getModel().get("name")); } @Test public void modelAttributeMethodWithNameByConvention() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttrConvention"); - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); - assertEquals(Boolean.TRUE, mavContainer.getModel().get("boolean")); + assertEquals(Boolean.TRUE, this.mavContainer.getModel().get("boolean")); } @Test public void modelAttributeMethodWithNullReturnValue() throws Exception { ModelFactory modelFactory = createModelFactory("nullModelAttr"); - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); - assertTrue(mavContainer.containsAttribute("name")); - assertNull(mavContainer.getModel().get("name")); + assertTrue(this.mavContainer.containsAttribute("name")); + assertNull(this.mavContainer.getModel().get("name")); } @Test - public void sessionAttribute() throws Exception { - this.sessionAttributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); + public void modelAttributeWithBindingDisabled() throws Exception { + ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled"); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); + + assertTrue(this.mavContainer.containsAttribute("foo")); + assertTrue(this.mavContainer.isBindingDisabled("foo")); + } + + @Test + public void modelAttributeFromSessionWithBindingDisabled() throws Exception { + Foo foo = new Foo(); + this.attributeStore.storeAttribute(this.webRequest, "foo", foo); - // Resolve successfully handler session attribute once - assertTrue(sessionAttrsHandler.isHandlerSessionAttribute("sessionAttr", null)); + ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled"); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); + + assertTrue(this.mavContainer.containsAttribute("foo")); + assertSame(foo, this.mavContainer.getModel().get("foo")); + assertTrue(this.mavContainer.isBindingDisabled("foo")); + } + + @Test + public void sessionAttribute() throws Exception { + this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); ModelFactory modelFactory = createModelFactory("modelAttr", Model.class); - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(this.webRequest, mavContainer, this.handleMethod); + HandlerMethod handlerMethod = createHandlerMethod("handle"); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); - assertEquals("sessionAttrValue", mavContainer.getModel().get("sessionAttr")); + assertEquals("sessionAttrValue", this.mavContainer.getModel().get("sessionAttr")); } @Test public void sessionAttributeNotPresent() throws Exception { - ModelFactory modelFactory = new ModelFactory(null, null, this.sessionAttrsHandler); - + ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler); + HandlerMethod handlerMethod = createHandlerMethod("handleSessionAttr", String.class); try { - modelFactory.initModel(this.webRequest, new ModelAndViewContainer(), this.handleSessionAttrMethod); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); fail("Expected HttpSessionRequiredException"); } catch (HttpSessionRequiredException e) { // expected } - this.sessionAttributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(this.webRequest, mavContainer, this.handleSessionAttrMethod); + // Now add attribute and try again + this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); - assertEquals("sessionAttrValue", mavContainer.getModel().get("sessionAttr")); + modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); + assertEquals("sessionAttrValue", this.mavContainer.getModel().get("sessionAttr")); } @Test @@ -165,11 +180,12 @@ public class ModelFactoryTests { WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); given(binderFactory.createBinder(this.webRequest, command, commandName)).willReturn(dataBinder); - ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler); + ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler); modelFactory.updateModel(this.webRequest, container); assertEquals(command, container.getModel().get(commandName)); - assertSame(dataBinder.getBindingResult(), container.getModel().get(bindingResultKey(commandName))); + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + commandName; + assertSame(dataBinder.getBindingResult(), container.getModel().get(bindingResultKey)); assertEquals(2, container.getModel().size()); } @@ -184,11 +200,11 @@ public class ModelFactoryTests { WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); given(binderFactory.createBinder(this.webRequest, attribute, attributeName)).willReturn(dataBinder); - ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler); + ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler); modelFactory.updateModel(this.webRequest, container); assertEquals(attribute, container.getModel().get(attributeName)); - assertEquals(attribute, this.sessionAttributeStore.retrieveAttribute(this.webRequest, attributeName)); + assertEquals(attribute, this.attributeStore.retrieveAttribute(this.webRequest, attributeName)); } @Test @@ -198,9 +214,7 @@ public class ModelFactoryTests { ModelAndViewContainer container = new ModelAndViewContainer(); container.addAttribute(attributeName, attribute); - // Store and resolve once (to be "remembered") - this.sessionAttributeStore.storeAttribute(this.webRequest, attributeName, attribute); - this.sessionAttrsHandler.isHandlerSessionAttribute(attributeName, null); + this.attributeStore.storeAttribute(this.webRequest, attributeName, attribute); WebDataBinder dataBinder = new WebDataBinder(attribute, attributeName); WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); @@ -208,11 +222,11 @@ public class ModelFactoryTests { container.getSessionStatus().setComplete(); - ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler); + ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler); modelFactory.updateModel(this.webRequest, container); assertEquals(attribute, container.getModel().get(attributeName)); - assertNull(this.sessionAttributeStore.retrieveAttribute(this.webRequest, attributeName)); + assertNull(this.attributeStore.retrieveAttribute(this.webRequest, attributeName)); } // SPR-12542 @@ -233,34 +247,34 @@ public class ModelFactoryTests { WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); given(binderFactory.createBinder(this.webRequest, attribute, attributeName)).willReturn(dataBinder); - ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.sessionAttrsHandler); + ModelFactory modelFactory = new ModelFactory(null, binderFactory, this.attributeHandler); modelFactory.updateModel(this.webRequest, container); assertEquals(queryParam, container.getModel().get(queryParamName)); assertEquals(1, container.getModel().size()); - assertEquals(attribute, this.sessionAttributeStore.retrieveAttribute(this.webRequest, attributeName)); + assertEquals(attribute, this.attributeStore.retrieveAttribute(this.webRequest, attributeName)); } - private String bindingResultKey(String key) { - return BindingResult.MODEL_KEY_PREFIX + key; - } - - private ModelFactory createModelFactory(String methodName, Class<?>... parameterTypes) throws Exception{ - Method method = TestController.class.getMethod(methodName, parameterTypes); + private ModelFactory createModelFactory(String methodName, Class<?>... parameterTypes) throws Exception { + HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); + resolvers.addResolver(new ModelMethodProcessor()); - HandlerMethodArgumentResolverComposite argResolvers = new HandlerMethodArgumentResolverComposite(); - argResolvers.addResolver(new ModelMethodProcessor()); + InvocableHandlerMethod modelMethod = createHandlerMethod(methodName, parameterTypes); + modelMethod.setHandlerMethodArgumentResolvers(resolvers); + modelMethod.setDataBinderFactory(null); + modelMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); - InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(this.controller, method); - handlerMethod.setHandlerMethodArgumentResolvers(argResolvers); - handlerMethod.setDataBinderFactory(null); - handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + return new ModelFactory(Collections.singletonList(modelMethod), null, this.attributeHandler); + } - return new ModelFactory(Arrays.asList(handlerMethod), null, this.sessionAttrsHandler); + private InvocableHandlerMethod createHandlerMethod(String methodName, Class<?>... paramTypes) throws Exception { + Method method = this.controller.getClass().getMethod(methodName, paramTypes); + return new InvocableHandlerMethod(this.controller, method); } - @SessionAttributes("sessionAttr") @SuppressWarnings("unused") + + @SessionAttributes({"sessionAttr", "foo"}) @SuppressWarnings("unused") private static class TestController { @ModelAttribute @@ -283,6 +297,11 @@ public class ModelFactoryTests { return null; } + @ModelAttribute(name="foo", binding=false) + public Foo modelAttrWithBindingDisabled() { + return new Foo(); + } + public void handle() { } @@ -290,4 +309,7 @@ public class ModelFactoryTests { } } + private static class Foo { + } + } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java index b484e2ff..a453abb0 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,9 @@ package org.springframework.web.method.annotation; import java.lang.reflect.Method; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; import java.util.Map; import org.junit.After; @@ -25,10 +28,14 @@ import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.DefaultDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; @@ -50,7 +57,11 @@ public class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramNamedValueStringArray; private MethodParameter paramSystemProperty; private MethodParameter paramContextPath; + private MethodParameter paramResolvedNameWithExpression; + private MethodParameter paramResolvedNameWithPlaceholder; private MethodParameter paramNamedValueMap; + private MethodParameter paramDate; + private MethodParameter paramInstant; private MockHttpServletRequest servletRequest; @@ -64,12 +75,16 @@ public class RequestHeaderMethodArgumentResolverTests { context.refresh(); resolver = new RequestHeaderMethodArgumentResolver(context.getBeanFactory()); - Method method = getClass().getMethod("params", String.class, String[].class, String.class, String.class, Map.class); + Method method = ReflectionUtils.findMethod(getClass(), "params", (Class<?>[]) null); paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 0); paramNamedValueStringArray = new SynthesizingMethodParameter(method, 1); paramSystemProperty = new SynthesizingMethodParameter(method, 2); paramContextPath = new SynthesizingMethodParameter(method, 3); - paramNamedValueMap = new SynthesizingMethodParameter(method, 4); + paramResolvedNameWithExpression = new SynthesizingMethodParameter(method, 4); + paramResolvedNameWithPlaceholder = new SynthesizingMethodParameter(method, 5); + paramNamedValueMap = new SynthesizingMethodParameter(method, 6); + paramDate = new SynthesizingMethodParameter(method, 7); + paramInstant = new SynthesizingMethodParameter(method, 8); servletRequest = new MockHttpServletRequest(); webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); @@ -97,45 +112,77 @@ public class RequestHeaderMethodArgumentResolverTests { servletRequest.addHeader("name", expected); Object result = resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, webRequest, null); - assertTrue(result instanceof String); - assertEquals("Invalid result", expected, result); + assertEquals(expected, result); } @Test public void resolveStringArrayArgument() throws Exception { - String[] expected = new String[]{"foo", "bar"}; + String[] expected = new String[] {"foo", "bar"}; servletRequest.addHeader("name", expected); Object result = resolver.resolveArgument(paramNamedValueStringArray, null, webRequest, null); - assertTrue(result instanceof String[]); - assertArrayEquals("Invalid result", expected, (String[]) result); + assertArrayEquals(expected, (String[]) result); } @Test public void resolveDefaultValue() throws Exception { Object result = resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, webRequest, null); - assertTrue(result instanceof String); - assertEquals("Invalid result", "bar", result); + assertEquals("bar", result); } @Test public void resolveDefaultValueFromSystemProperty() throws Exception { System.setProperty("systemProperty", "bar"); - Object result = resolver.resolveArgument(paramSystemProperty, null, webRequest, null); - System.clearProperty("systemProperty"); + try { + Object result = resolver.resolveArgument(paramSystemProperty, null, webRequest, null); + assertTrue(result instanceof String); + assertEquals("bar", result); + } + finally { + System.clearProperty("systemProperty"); + } + } - assertTrue(result instanceof String); - assertEquals("bar", result); + @Test + public void resolveNameFromSystemPropertyThroughExpression() throws Exception { + String expected = "foo"; + servletRequest.addHeader("bar", expected); + + System.setProperty("systemProperty", "bar"); + try { + Object result = resolver.resolveArgument(paramResolvedNameWithExpression, null, webRequest, null); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { + String expected = "foo"; + servletRequest.addHeader("bar", expected); + + System.setProperty("systemProperty", "bar"); + try { + Object result = resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, webRequest, null); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + finally { + System.clearProperty("systemProperty"); + } } @Test public void resolveDefaultValueFromRequest() throws Exception { servletRequest.setContextPath("/bar"); - Object result = resolver.resolveArgument(paramContextPath, null, webRequest, null); + Object result = resolver.resolveArgument(paramContextPath, null, webRequest, null); assertTrue(result instanceof String); assertEquals("/bar", result); } @@ -145,12 +192,46 @@ public class RequestHeaderMethodArgumentResolverTests { resolver.resolveArgument(paramNamedValueStringArray, null, webRequest, null); } + @Test + @SuppressWarnings("deprecation") + public void dateConversion() throws Exception { + String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; + servletRequest.addHeader("name", rfc1123val); + + ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + Object result = resolver.resolveArgument(paramDate, null, webRequest, + new DefaultDataBinderFactory(bindingInitializer)); + + assertTrue(result instanceof Date); + assertEquals(new Date(rfc1123val), result); + } + + @Test + public void instantConversion() throws Exception { + String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; + servletRequest.addHeader("name", rfc1123val); + + ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer(); + bindingInitializer.setConversionService(new DefaultFormattingConversionService()); + Object result = resolver.resolveArgument(paramInstant, null, webRequest, + new DefaultDataBinderFactory(bindingInitializer)); + + assertTrue(result instanceof Instant); + assertEquals(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val)), result); + } + - public void params(@RequestHeader(name = "name", defaultValue = "bar") String param1, - @RequestHeader("name") String[] param2, - @RequestHeader(name = "name", defaultValue="#{systemProperties.systemProperty}") String param3, - @RequestHeader(name = "name", defaultValue="#{request.contextPath}") String param4, - @RequestHeader("name") Map<?, ?> unsupported) { + public void params( + @RequestHeader(name = "name", defaultValue = "bar") String param1, + @RequestHeader("name") String[] param2, + @RequestHeader(name = "name", defaultValue="#{systemProperties.systemProperty}") String param3, + @RequestHeader(name = "name", defaultValue="#{request.contextPath}") String param4, + @RequestHeader("#{systemProperties.systemProperty}") String param5, + @RequestHeader("${systemProperty}") String param6, + @RequestHeader("name") Map<?, ?> unsupported, + @RequestHeader("name") Date dateParam, + @RequestHeader("name") Instant instantParam) { } } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java index 00b61b5f..f77ce2bc 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -37,6 +37,7 @@ import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockMultipartFile; import org.springframework.mock.web.test.MockMultipartHttpServletRequest; import org.springframework.mock.web.test.MockPart; +import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestParam; @@ -49,6 +50,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.support.MissingServletRequestPartException; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; @@ -82,6 +84,7 @@ public class RequestParamMethodArgumentResolverTests { private MethodParameter paramRequired; private MethodParameter paramNotRequired; private MethodParameter paramOptional; + private MethodParameter multipartFileOptional; private NativeWebRequest webRequest; @@ -92,12 +95,7 @@ public class RequestParamMethodArgumentResolverTests { public void setUp() throws Exception { resolver = new RequestParamMethodArgumentResolver(null, true); ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); - - Method method = getClass().getMethod("params", String.class, String[].class, - Map.class, MultipartFile.class, List.class, MultipartFile[].class, - Part.class, List.class, Part[].class, Map.class, - String.class, MultipartFile.class, List.class, Part.class, - MultipartFile.class, String.class, String.class, Optional.class); + Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class<?>[]) null); paramNamedDefaultValueString = new SynthesizingMethodParameter(method, 0); paramNamedStringArray = new SynthesizingMethodParameter(method, 1); @@ -121,6 +119,7 @@ public class RequestParamMethodArgumentResolverTests { paramRequired = new SynthesizingMethodParameter(method, 15); paramNotRequired = new SynthesizingMethodParameter(method, 16); paramOptional = new SynthesizingMethodParameter(method, 17); + multipartFileOptional = new SynthesizingMethodParameter(method, 18); request = new MockHttpServletRequest(); webRequest = new ServletWebRequest(request, new MockHttpServletResponse()); @@ -130,19 +129,25 @@ public class RequestParamMethodArgumentResolverTests { @Test public void supportsParameter() { resolver = new RequestParamMethodArgumentResolver(null, true); - assertTrue("String parameter not supported", resolver.supportsParameter(paramNamedDefaultValueString)); - assertTrue("String array parameter not supported", resolver.supportsParameter(paramNamedStringArray)); - assertTrue("Named map not parameter supported", resolver.supportsParameter(paramNamedMap)); - assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFile)); - assertTrue("List<MultipartFile> parameter not supported", resolver.supportsParameter(paramMultipartFileList)); - assertTrue("MultipartFile[] parameter not supported", resolver.supportsParameter(paramMultipartFileArray)); - assertTrue("Part parameter not supported", resolver.supportsParameter(paramPart)); - assertTrue("List<Part> parameter not supported", resolver.supportsParameter(paramPartList)); - assertTrue("Part[] parameter not supported", resolver.supportsParameter(paramPartArray)); - assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(paramMap)); - assertTrue("Simple type params supported w/o annotations", resolver.supportsParameter(paramStringNotAnnot)); - assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFileNotAnnot)); - assertTrue("Part parameter not supported", resolver.supportsParameter(paramPartNotAnnot)); + assertTrue(resolver.supportsParameter(paramNamedDefaultValueString)); + assertTrue(resolver.supportsParameter(paramNamedStringArray)); + assertTrue(resolver.supportsParameter(paramNamedMap)); + assertTrue(resolver.supportsParameter(paramMultipartFile)); + assertTrue(resolver.supportsParameter(paramMultipartFileList)); + assertTrue(resolver.supportsParameter(paramMultipartFileArray)); + assertTrue(resolver.supportsParameter(paramPart)); + assertTrue(resolver.supportsParameter(paramPartList)); + assertTrue(resolver.supportsParameter(paramPartArray)); + assertFalse(resolver.supportsParameter(paramMap)); + assertTrue(resolver.supportsParameter(paramStringNotAnnot)); + assertTrue(resolver.supportsParameter(paramMultipartFileNotAnnot)); + assertTrue(resolver.supportsParameter(paramMultipartFileListNotAnnot)); + assertTrue(resolver.supportsParameter(paramPartNotAnnot)); + assertFalse(resolver.supportsParameter(paramRequestPartAnnot)); + assertTrue(resolver.supportsParameter(paramRequired)); + assertTrue(resolver.supportsParameter(paramNotRequired)); + assertTrue(resolver.supportsParameter(paramOptional)); + assertTrue(resolver.supportsParameter(multipartFileOptional)); resolver = new RequestParamMethodArgumentResolver(null, false); assertFalse(resolver.supportsParameter(paramStringNotAnnot)); @@ -188,6 +193,7 @@ public class RequestParamMethodArgumentResolverTests { MultipartFile expected2 = new MockMultipartFile("mfilelist", "Hello World 2".getBytes()); request.addFile(expected1); request.addFile(expected2); + request.addFile(new MockMultipartFile("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileList, null, webRequest, null); @@ -202,6 +208,7 @@ public class RequestParamMethodArgumentResolverTests { MultipartFile expected2 = new MockMultipartFile("mfilearray", "Hello World 2".getBytes()); request.addFile(expected1); request.addFile(expected2); + request.addFile(new MockMultipartFile("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileArray, null, webRequest, null); @@ -222,7 +229,6 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPart, null, webRequest, null); - assertTrue(result instanceof Part); assertEquals("Invalid result", expected, result); } @@ -230,12 +236,13 @@ public class RequestParamMethodArgumentResolverTests { @Test public void resolvePartList() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); - MockPart expected1 = new MockPart("pfilelist", "Hello World 1".getBytes()); - MockPart expected2 = new MockPart("pfilelist", "Hello World 2".getBytes()); request.setMethod("POST"); request.setContentType("multipart/form-data"); + MockPart expected1 = new MockPart("pfilelist", "Hello World 1".getBytes()); + MockPart expected2 = new MockPart("pfilelist", "Hello World 2".getBytes()); request.addPart(expected1); request.addPart(expected2); + request.addPart(new MockPart("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartList, null, webRequest, null); @@ -252,6 +259,7 @@ public class RequestParamMethodArgumentResolverTests { request.setContentType("multipart/form-data"); request.addPart(expected1); request.addPart(expected2); + request.addPart(new MockPart("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartArray, null, webRequest, null); @@ -307,12 +315,19 @@ public class RequestParamMethodArgumentResolverTests { assertEquals(expected, ((List<?>) actual).get(0)); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = MultipartException.class) + public void noMultipartContent() throws Exception { + request.setMethod("POST"); + resolver.resolveArgument(paramMultipartFile, null, webRequest, null); + fail("Expected exception: no multipart content"); + } + + @Test(expected = MissingServletRequestPartException.class) public void missingMultipartFile() throws Exception { request.setMethod("POST"); request.setContentType("multipart/form-data"); resolver.resolveArgument(paramMultipartFile, null, webRequest, null); - fail("Expected exception: request is not MultiPartHttpServletRequest but param is MultipartFile"); + fail("Expected exception: no such part found"); } @Test @@ -422,8 +437,45 @@ public class RequestParamMethodArgumentResolverTests { assertEquals(123, ((Optional) result).get()); } + @Test + public void resolveOptionalMultipartFile() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + MultipartFile expected = new MockMultipartFile("mfile", "Hello World".getBytes()); + request.addFile(expected); + webRequest = new ServletWebRequest(request); + + Object result = resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory); + assertTrue(result instanceof Optional); + assertEquals("Invalid result", expected, ((Optional<?>) result).get()); + } + + @Test + public void missingOptionalMultipartFile() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + assertEquals(Optional.empty(), resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory)); + } + + @Test + public void optionalMultipartFileWithoutMultipartRequest() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + assertEquals(Optional.empty(), resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory)); + } + - public void params(@RequestParam(name = "name", defaultValue = "bar") String param1, + public void handle( + @RequestParam(name = "name", defaultValue = "bar") String param1, @RequestParam("name") String[] param2, @RequestParam("name") Map<?, ?> param3, @RequestParam("mfile") MultipartFile param4, @@ -440,7 +492,8 @@ public class RequestParamMethodArgumentResolverTests { @RequestPart MultipartFile requestPartAnnot, @RequestParam("name") String paramRequired, @RequestParam(name = "name", required = false) String paramNotRequired, - @RequestParam("name") Optional<Integer> paramOptional) { + @RequestParam("name") Optional<Integer> paramOptional, + @RequestParam("mfile") Optional<MultipartFile> multipartFileOptional) { } } diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java index 1642351e..09f3700a 100644 --- a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,74 +15,135 @@ */ package org.springframework.web.util; -import static org.junit.Assert.assertEquals; - import java.net.URI; import java.util.HashMap; import java.util.Map; -import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.assertEquals; + /** * Unit tests for {@link DefaultUriTemplateHandler}. + * * @author Rossen Stoyanchev */ public class DefaultUriTemplateHandlerTests { - private DefaultUriTemplateHandler handler; - - - @Before - public void setUp() throws Exception { - this.handler = new DefaultUriTemplateHandler(); - } + private final DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler(); @Test - public void baseUrl() throws Exception { + public void baseUrlWithoutPath() throws Exception { this.handler.setBaseUrl("http://localhost:8080"); URI actual = this.handler.expand("/myapiresource"); - URI expected = new URI("http://localhost:8080/myapiresource"); - assertEquals(expected, actual); + assertEquals("http://localhost:8080/myapiresource", actual.toString()); } @Test - public void baseUrlWithPartialPath() throws Exception { + public void baseUrlWithPath() throws Exception { this.handler.setBaseUrl("http://localhost:8080/context"); URI actual = this.handler.expand("/myapiresource"); - URI expected = new URI("http://localhost:8080/context/myapiresource"); - assertEquals(expected, actual); + assertEquals("http://localhost:8080/context/myapiresource", actual.toString()); + } + + @Test // SPR-14147 + public void defaultUriVariables() throws Exception { + Map<String, String> defaultVars = new HashMap<>(2); + defaultVars.put("host", "api.example.com"); + defaultVars.put("port", "443"); + this.handler.setDefaultUriVariables(defaultVars); + + Map<String, Object> vars = new HashMap<>(1); + vars.put("id", 123L); + + String template = "https://{host}:{port}/v42/customers/{id}"; + URI actual = this.handler.expand(template, vars); + + assertEquals("https://api.example.com:443/v42/customers/123", actual.toString()); } @Test - public void expandWithFullPath() throws Exception { - Map<String, String> vars = new HashMap<String, String>(2); + public void parsePathIsOff() throws Exception { + this.handler.setParsePath(false); + Map<String, String> vars = new HashMap<>(2); vars.put("hotel", "1"); vars.put("publicpath", "pics/logo.png"); String template = "http://example.com/hotels/{hotel}/pic/{publicpath}"; - URI actual = this.handler.expand(template, vars); - URI expected = new URI("http://example.com/hotels/1/pic/pics/logo.png"); - assertEquals(expected, actual); + assertEquals("http://example.com/hotels/1/pic/pics/logo.png", actual.toString()); } @Test - public void expandWithFullPathAndParsePathEnabled() throws Exception { - Map<String, String> vars = new HashMap<String, String>(2); + public void parsePathIsOn() throws Exception { + this.handler.setParsePath(true); + Map<String, String> vars = new HashMap<>(2); vars.put("hotel", "1"); vars.put("publicpath", "pics/logo.png"); vars.put("scale", "150x150"); String template = "http://example.com/hotels/{hotel}/pic/{publicpath}/size/{scale}"; + URI actual = this.handler.expand(template, vars); - this.handler.setParsePath(true); + assertEquals("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150", actual.toString()); + } + + @Test + public void strictEncodingIsOffWithMap() throws Exception { + this.handler.setStrictEncoding(false); + Map<String, String> vars = new HashMap<>(2); + vars.put("userId", "john;doe"); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, vars); + + assertEquals("http://www.example.com/user/john;doe/dashboard", actual.toString()); + } + + @Test + public void strictEncodingOffWithArray() throws Exception { + this.handler.setStrictEncoding(false); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, "john;doe"); + + assertEquals("http://www.example.com/user/john;doe/dashboard", actual.toString()); + } + + @Test + public void strictEncodingOnWithMap() throws Exception { + this.handler.setStrictEncoding(true); + Map<String, String> vars = new HashMap<>(2); + vars.put("userId", "john;doe"); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, vars); + + assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", actual.toString()); + } + + @Test + public void strictEncodingOnWithArray() throws Exception { + this.handler.setStrictEncoding(true); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, "john;doe"); + + assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", actual.toString()); + } + + @Test // SPR-14147 + public void strictEncodingAndDefaultUriVariables() throws Exception { + Map<String, String> defaultVars = new HashMap<>(1); + defaultVars.put("host", "www.example.com"); + this.handler.setDefaultUriVariables(defaultVars); + this.handler.setStrictEncoding(true); + + Map<String, Object> vars = new HashMap<>(1); + vars.put("userId", "john;doe"); + + String template = "http://{host}/user/{userId}/dashboard"; URI actual = this.handler.expand(template, vars); - URI expected = new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150"); - assertEquals(expected, actual); + assertEquals("http://www.example.com/user/john%3Bdoe/dashboard", actual.toString()); } } diff --git a/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java b/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java index 5001f9fb..c32405e4 100644 --- a/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/Log4jWebConfigurerTests.java @@ -101,7 +101,8 @@ public class Log4jWebConfigurerTests { try { assertLogOutput(); - } finally { + } + finally { Log4jWebConfigurer.shutdownLogging(sc); } assertTrue(MockLog4jAppender.closeCalled); @@ -132,7 +133,8 @@ public class Log4jWebConfigurerTests { try { assertLogOutput(); - } finally { + } + finally { listener.contextDestroyed(new ServletContextEvent(sc)); } assertTrue(MockLog4jAppender.closeCalled); diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java index a047af03..99c642f0 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,116 +30,126 @@ import static org.junit.Assert.*; /** * @author Arjen Poutsma * @author Juergen Hoeller + * @author Rossen Stoyanchev */ public class UriTemplateTests { @Test public void getVariableNames() throws Exception { - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); List<String> variableNames = template.getVariableNames(); assertEquals("Invalid variable names", Arrays.asList("hotel", "booking"), variableNames); } @Test public void expandVarArgs() throws Exception { - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand("1", "42"); - assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); + assertEquals("Invalid expanded template", new URI("/hotels/1/bookings/42"), result); + } + + // SPR-9712 + + @Test + public void expandVarArgsWithArrayValue() throws Exception { + UriTemplate template = new UriTemplate("/sum?numbers={numbers}"); + URI result = template.expand(new int[] {1, 2, 3}); + assertEquals(new URI("/sum?numbers=1,2,3"), result); } @Test(expected = IllegalArgumentException.class) public void expandVarArgsNotEnoughVariables() throws Exception { - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); template.expand("1"); } @Test public void expandMap() throws Exception { - Map<String, String> uriVariables = new HashMap<String, String>(2); + Map<String, String> uriVariables = new HashMap<>(2); uriVariables.put("booking", "42"); uriVariables.put("hotel", "1"); - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand(uriVariables); - assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); + assertEquals("Invalid expanded template", new URI("/hotels/1/bookings/42"), result); } @Test public void expandMapDuplicateVariables() throws Exception { UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}"); - assertEquals("Invalid variable names", Arrays.asList("c", "c", "c"), template.getVariableNames()); + assertEquals(Arrays.asList("c", "c", "c"), template.getVariableNames()); URI result = template.expand(Collections.singletonMap("c", "cheeseburger")); - assertEquals("Invalid expanded template", new URI("/order/cheeseburger/cheeseburger/cheeseburger"), result); + assertEquals(new URI("/order/cheeseburger/cheeseburger/cheeseburger"), result); } @Test public void expandMapNonString() throws Exception { - Map<String, Integer> uriVariables = new HashMap<String, Integer>(2); + Map<String, Integer> uriVariables = new HashMap<>(2); uriVariables.put("booking", 42); uriVariables.put("hotel", 1); - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); URI result = template.expand(uriVariables); - assertEquals("Invalid expanded template", new URI("http://example.com/hotels/1/bookings/42"), result); + assertEquals("Invalid expanded template", new URI("/hotels/1/bookings/42"), result); } @Test public void expandMapEncoded() throws Exception { Map<String, String> uriVariables = Collections.singletonMap("hotel", "Z\u00fcrich"); - UriTemplate template = new UriTemplate("http://example.com/hotel list/{hotel}"); + UriTemplate template = new UriTemplate("/hotel list/{hotel}"); URI result = template.expand(uriVariables); - assertEquals("Invalid expanded template", new URI("http://example.com/hotel%20list/Z%C3%BCrich"), result); + assertEquals("Invalid expanded template", new URI("/hotel%20list/Z%C3%BCrich"), result); } @Test(expected = IllegalArgumentException.class) public void expandMapUnboundVariables() throws Exception { - Map<String, String> uriVariables = new HashMap<String, String>(2); + Map<String, String> uriVariables = new HashMap<>(2); uriVariables.put("booking", "42"); uriVariables.put("bar", "1"); - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); template.expand(uriVariables); } @Test public void expandEncoded() throws Exception { - UriTemplate template = new UriTemplate("http://example.com/hotel list/{hotel}"); + UriTemplate template = new UriTemplate("/hotel list/{hotel}"); URI result = template.expand("Z\u00fcrich"); - assertEquals("Invalid expanded template", new URI("http://example.com/hotel%20list/Z%C3%BCrich"), result); + assertEquals("Invalid expanded template", new URI("/hotel%20list/Z%C3%BCrich"), result); } @Test public void matches() throws Exception { - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); - assertTrue("UriTemplate does not match", template.matches("http://example.com/hotels/1/bookings/42")); - assertFalse("UriTemplate matches", template.matches("http://example.com/hotels/bookings")); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); + assertTrue("UriTemplate does not match", template.matches("/hotels/1/bookings/42")); + assertFalse("UriTemplate matches", template.matches("/hotels/bookings")); assertFalse("UriTemplate matches", template.matches("")); assertFalse("UriTemplate matches", template.matches(null)); } @Test public void matchesCustomRegex() throws Exception { - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel:\\d+}"); - assertTrue("UriTemplate does not match", template.matches("http://example.com/hotels/42")); - assertFalse("UriTemplate matches", template.matches("http://example.com/hotels/foo")); + UriTemplate template = new UriTemplate("/hotels/{hotel:\\d+}"); + assertTrue("UriTemplate does not match", template.matches("/hotels/42")); + assertFalse("UriTemplate matches", template.matches("/hotels/foo")); } @Test public void match() throws Exception { - Map<String, String> expected = new HashMap<String, String>(2); + Map<String, String> expected = new HashMap<>(2); expected.put("booking", "42"); expected.put("hotel", "1"); - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel}/bookings/{booking}"); - Map<String, String> result = template.match("http://example.com/hotels/1/bookings/42"); + UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); + Map<String, String> result = template.match("/hotels/1/bookings/42"); assertEquals("Invalid match", expected, result); } @Test public void matchCustomRegex() throws Exception { - Map<String, String> expected = new HashMap<String, String>(2); + Map<String, String> expected = new HashMap<>(2); expected.put("booking", "42"); expected.put("hotel", "1"); - UriTemplate template = new UriTemplate("http://example.com/hotels/{hotel:\\d}/bookings/{booking:\\d+}"); - Map<String, String> result = template.match("http://example.com/hotels/1/bookings/42"); + UriTemplate template = new UriTemplate("/hotels/{hotel:\\d}/bookings/{booking:\\d+}"); + Map<String, String> result = template.match("/hotels/1/bookings/42"); assertEquals("Invalid match", expected, result); } @@ -164,7 +174,7 @@ public class UriTemplateTests { public void matchMultipleInOneSegment() throws Exception { UriTemplate template = new UriTemplate("/{foo}-{bar}"); Map<String, String> result = template.match("/12-34"); - Map<String, String> expected = new HashMap<String, String>(2); + Map<String, String> expected = new HashMap<>(2); expected.put("foo", "12"); expected.put("bar", "34"); assertEquals("Invalid match", expected, result); diff --git a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java index 6480c5fa..2227674a 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import static org.junit.Assert.*; /** * @author Arjen Poutsma + * @author Juergen Hoeller */ public class UriUtilsTests { @@ -104,4 +105,22 @@ public class UriUtilsTests { UriUtils.decode("foo%2", ENC); } + @Test + public void extractFileExtension() { + assertEquals("html", UriUtils.extractFileExtension("index.html")); + assertEquals("html", UriUtils.extractFileExtension("/index.html")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html#/a")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html#/path/a")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html#/path/a.do")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=a")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a.do")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a#/path/a")); + assertEquals("html", UriUtils.extractFileExtension("/products/view.html?param=/path/a.do#/path/a.do")); + assertEquals("html", UriUtils.extractFileExtension("/products;q=11/view.html?param=/path/a.do")); + assertEquals("html", UriUtils.extractFileExtension("/products;q=11/view.html;r=22?param=/path/a.do")); + assertEquals("html", UriUtils.extractFileExtension("/products;q=11/view.html;r=22;s=33?param=/path/a.do")); + } + } diff --git a/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt b/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt new file mode 100644 index 00000000..84bbb9dd --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/converter/byterangeresource.txt @@ -0,0 +1 @@ +Spring Framework test resource content.
\ No newline at end of file diff --git a/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd b/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd index 31aa2524..86e8cbab 100644 --- a/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd +++ b/spring-web/src/test/resources/org/springframework/web/util/HtmlCharacterEntityReferences.dtd @@ -1,11 +1,9 @@ -<!-- File containing all charcter entity references definied +<!-- File containing all character entity references defined by the HTML 4.0 standard. --> -<!-- Valuable informations and a complete description of the +<!-- Valuable information and a complete description of the HTML 4.0 character set can be found at - http://www.w3.org/TR/html4/charset.html. - --> - - + http://www.w3.org/TR/html4/charset.html. --> + <!-- Portions © International Organization for Standardization 1986 Permission to copy in any form is granted for use with conforming SGML systems and applications as defined in |