/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.http; import java.io.Serializable; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; 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 java.util.stream.Collectors; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** * Represents HTTP request and response headers, mapping string header names to a list of string values. * * <p>In addition to the normal methods defined by {@link Map}, this class offers the following * convenience methods: * <ul> * <li>{@link #getFirst(String)} returns the first value associated with a given header name</li> * <li>{@link #add(String, String)} adds a header value to the list of values for a header name</li> * <li>{@link #set(String, String)} sets the header value to a single string value</li> * </ul> * * <p>Inspired by {@code com.sun.net.httpserver.Headers}. * * @author Arjen Poutsma * @author Sebastien Deleuze * @author Brian Clozel * @author Juergen Hoeller * @author Josh Long * @since 3.0 */ public class HttpHeaders implements MultiValueMap<String, String>, Serializable { private static final long serialVersionUID = -8578554704772377436L; /** * The HTTP {@code Accept} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.2">Section 5.3.2 of RFC 7231</a> */ public static final String ACCEPT = "Accept"; /** * The HTTP {@code Accept-Charset} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.3">Section 5.3.3 of RFC 7231</a> */ public static final String ACCEPT_CHARSET = "Accept-Charset"; /** * The HTTP {@code Accept-Encoding} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.4">Section 5.3.4 of RFC 7231</a> */ public static final String ACCEPT_ENCODING = "Accept-Encoding"; /** * The HTTP {@code Accept-Language} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.5">Section 5.3.5 of RFC 7231</a> */ public static final String ACCEPT_LANGUAGE = "Accept-Language"; /** * The HTTP {@code Accept-Ranges} header field name. * @see <a href="http://tools.ietf.org/html/rfc7233#section-2.3">Section 5.3.5 of RFC 7233</a> */ public static final String ACCEPT_RANGES = "Accept-Ranges"; /** * The CORS {@code Access-Control-Allow-Credentials} response header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; /** * The CORS {@code Access-Control-Allow-Headers} response header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; /** * The CORS {@code Access-Control-Allow-Methods} response header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; /** * The CORS {@code Access-Control-Allow-Origin} response header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; /** * The CORS {@code Access-Control-Expose-Headers} response header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; /** * The CORS {@code Access-Control-Max-Age} response header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; /** * The CORS {@code Access-Control-Request-Headers} request header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; /** * The CORS {@code Access-Control-Request-Method} request header field name. * @see <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a> */ public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; /** * The HTTP {@code Age} header field name. * @see <a href="http://tools.ietf.org/html/rfc7234#section-5.1">Section 5.1 of RFC 7234</a> */ public static final String AGE = "Age"; /** * The HTTP {@code Allow} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-7.4.1">Section 7.4.1 of RFC 7231</a> */ public static final String ALLOW = "Allow"; /** * The HTTP {@code Authorization} header field name. * @see <a href="http://tools.ietf.org/html/rfc7235#section-4.2">Section 4.2 of RFC 7235</a> */ public static final String AUTHORIZATION = "Authorization"; /** * The HTTP {@code Cache-Control} header field name. * @see <a href="http://tools.ietf.org/html/rfc7234#section-5.2">Section 5.2 of RFC 7234</a> */ public static final String CACHE_CONTROL = "Cache-Control"; /** * The HTTP {@code Connection} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-6.1">Section 6.1 of RFC 7230</a> */ public static final String CONNECTION = "Connection"; /** * The HTTP {@code Content-Encoding} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.2.2">Section 3.1.2.2 of RFC 7231</a> */ public static final String CONTENT_ENCODING = "Content-Encoding"; /** * The HTTP {@code Content-Disposition} header field name * @see <a href="http://tools.ietf.org/html/rfc6266">RFC 6266</a> */ public static final String CONTENT_DISPOSITION = "Content-Disposition"; /** * The HTTP {@code Content-Language} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.3.2">Section 3.1.3.2 of RFC 7231</a> */ public static final String CONTENT_LANGUAGE = "Content-Language"; /** * The HTTP {@code Content-Length} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-3.3.2">Section 3.3.2 of RFC 7230</a> */ public static final String CONTENT_LENGTH = "Content-Length"; /** * The HTTP {@code Content-Location} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.4.2">Section 3.1.4.2 of RFC 7231</a> */ public static final String CONTENT_LOCATION = "Content-Location"; /** * The HTTP {@code Content-Range} header field name. * @see <a href="http://tools.ietf.org/html/rfc7233#section-4.2">Section 4.2 of RFC 7233</a> */ public static final String CONTENT_RANGE = "Content-Range"; /** * The HTTP {@code Content-Type} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-3.1.1.5">Section 3.1.1.5 of RFC 7231</a> */ public static final String CONTENT_TYPE = "Content-Type"; /** * The HTTP {@code Cookie} header field name. * @see <a href="http://tools.ietf.org/html/rfc2109#section-4.3.4">Section 4.3.4 of RFC 2109</a> */ public static final String COOKIE = "Cookie"; /** * The HTTP {@code Date} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-7.1.1.2">Section 7.1.1.2 of RFC 7231</a> */ public static final String DATE = "Date"; /** * The HTTP {@code ETag} header field name. * @see <a href="http://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> */ public static final String ETAG = "ETag"; /** * The HTTP {@code Expect} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.1.1">Section 5.1.1 of RFC 7231</a> */ public static final String EXPECT = "Expect"; /** * The HTTP {@code Expires} header field name. * @see <a href="http://tools.ietf.org/html/rfc7234#section-5.3">Section 5.3 of RFC 7234</a> */ public static final String EXPIRES = "Expires"; /** * The HTTP {@code From} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.5.1">Section 5.5.1 of RFC 7231</a> */ public static final String FROM = "From"; /** * The HTTP {@code Host} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-5.4">Section 5.4 of RFC 7230</a> */ public static final String HOST = "Host"; /** * The HTTP {@code If-Match} header field name. * @see <a href="http://tools.ietf.org/html/rfc7232#section-3.1">Section 3.1 of RFC 7232</a> */ public static final String IF_MATCH = "If-Match"; /** * The HTTP {@code If-Modified-Since} header field name. * @see <a href="http://tools.ietf.org/html/rfc7232#section-3.3">Section 3.3 of RFC 7232</a> */ public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; /** * The HTTP {@code If-None-Match} header field name. * @see <a href="http://tools.ietf.org/html/rfc7232#section-3.2">Section 3.2 of RFC 7232</a> */ public static final String IF_NONE_MATCH = "If-None-Match"; /** * The HTTP {@code If-Range} header field name. * @see <a href="http://tools.ietf.org/html/rfc7233#section-3.2">Section 3.2 of RFC 7233</a> */ public static final String IF_RANGE = "If-Range"; /** * The HTTP {@code If-Unmodified-Since} header field name. * @see <a href="http://tools.ietf.org/html/rfc7232#section-3.4">Section 3.4 of RFC 7232</a> */ public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; /** * The HTTP {@code Last-Modified} header field name. * @see <a href="http://tools.ietf.org/html/rfc7232#section-2.2">Section 2.2 of RFC 7232</a> */ public static final String LAST_MODIFIED = "Last-Modified"; /** * The HTTP {@code Link} header field name. * @see <a href="http://tools.ietf.org/html/rfc5988">RFC 5988</a> */ public static final String LINK = "Link"; /** * The HTTP {@code Location} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-7.1.2">Section 7.1.2 of RFC 7231</a> */ public static final String LOCATION = "Location"; /** * The HTTP {@code Max-Forwards} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.1.2">Section 5.1.2 of RFC 7231</a> */ public static final String MAX_FORWARDS = "Max-Forwards"; /** * The HTTP {@code Origin} header field name. * @see <a href="http://tools.ietf.org/html/rfc6454">RFC 6454</a> */ public static final String ORIGIN = "Origin"; /** * The HTTP {@code Pragma} header field name. * @see <a href="http://tools.ietf.org/html/rfc7234#section-5.4">Section 5.4 of RFC 7234</a> */ public static final String PRAGMA = "Pragma"; /** * The HTTP {@code Proxy-Authenticate} header field name. * @see <a href="http://tools.ietf.org/html/rfc7235#section-4.3">Section 4.3 of RFC 7235</a> */ public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; /** * The HTTP {@code Proxy-Authorization} header field name. * @see <a href="http://tools.ietf.org/html/rfc7235#section-4.4">Section 4.4 of RFC 7235</a> */ public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; /** * The HTTP {@code Range} header field name. * @see <a href="http://tools.ietf.org/html/rfc7233#section-3.1">Section 3.1 of RFC 7233</a> */ public static final String RANGE = "Range"; /** * The HTTP {@code Referer} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.5.2">Section 5.5.2 of RFC 7231</a> */ public static final String REFERER = "Referer"; /** * The HTTP {@code Retry-After} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-7.1.3">Section 7.1.3 of RFC 7231</a> */ public static final String RETRY_AFTER = "Retry-After"; /** * The HTTP {@code Server} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-7.4.2">Section 7.4.2 of RFC 7231</a> */ public static final String SERVER = "Server"; /** * The HTTP {@code Set-Cookie} header field name. * @see <a href="http://tools.ietf.org/html/rfc2109#section-4.2.2">Section 4.2.2 of RFC 2109</a> */ public static final String SET_COOKIE = "Set-Cookie"; /** * The HTTP {@code Set-Cookie2} header field name. * @see <a href="http://tools.ietf.org/html/rfc2965">RFC 2965</a> */ public static final String SET_COOKIE2 = "Set-Cookie2"; /** * The HTTP {@code TE} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-4.3">Section 4.3 of RFC 7230</a> */ public static final String TE = "TE"; /** * The HTTP {@code Trailer} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-4.4">Section 4.4 of RFC 7230</a> */ public static final String TRAILER = "Trailer"; /** * The HTTP {@code Transfer-Encoding} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-3.3.1">Section 3.3.1 of RFC 7230</a> */ public static final String TRANSFER_ENCODING = "Transfer-Encoding"; /** * The HTTP {@code Upgrade} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-6.7">Section 6.7 of RFC 7230</a> */ public static final String UPGRADE = "Upgrade"; /** * The HTTP {@code User-Agent} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-5.5.3">Section 5.5.3 of RFC 7231</a> */ public static final String USER_AGENT = "User-Agent"; /** * The HTTP {@code Vary} header field name. * @see <a href="http://tools.ietf.org/html/rfc7231#section-7.1.4">Section 7.1.4 of RFC 7231</a> */ public static final String VARY = "Vary"; /** * The HTTP {@code Via} header field name. * @see <a href="http://tools.ietf.org/html/rfc7230#section-5.7.1">Section 5.7.1 of RFC 7230</a> */ public static final String VIA = "Via"; /** * The HTTP {@code Warning} header field name. * @see <a href="http://tools.ietf.org/html/rfc7234#section-5.5">Section 5.5 of RFC 7234</a> */ public static final String WARNING = "Warning"; /** * The HTTP {@code WWW-Authenticate} header field name. * @see <a href="http://tools.ietf.org/html/rfc7235#section-4.1">Section 4.1 of RFC 7235</a> */ public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; /** * Date formats as specified in the HTTP RFC * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> */ private static final String[] DATE_FORMATS = new String[] { "EEE, dd MMM yyyy HH:mm:ss zzz", "EEE, dd-MMM-yy HH:mm:ss zzz", "EEE MMM dd HH:mm:ss yyyy" }; /** * 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 final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH); private static TimeZone GMT = TimeZone.getTimeZone("GMT"); private final Map<String, List<String>> headers; /** * Constructs a new, empty instance of the {@code HttpHeaders} object. */ public HttpHeaders() { this(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH), false); } /** * Private constructor that can create read-only {@code HttpHeader} instances. */ private HttpHeaders(Map<String, List<String>> headers, boolean readOnly) { Assert.notNull(headers, "'headers' must not be null"); if (readOnly) { Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(headers.size(), Locale.ENGLISH); for (Entry<String, List<String>> entry : headers.entrySet()) { List<String> values = Collections.unmodifiableList(entry.getValue()); map.put(entry.getKey(), values); } this.headers = Collections.unmodifiableMap(map); } else { this.headers = headers; } } /** * Set the list of acceptable {@linkplain MediaType media types}, * as specified by the {@code Accept} header. */ public void setAccept(List<MediaType> acceptableMediaTypes) { set(ACCEPT, MediaType.toString(acceptableMediaTypes)); } /** * Return the list of acceptable {@linkplain MediaType media types}, * as specified by the {@code Accept} header. * <p>Returns an empty list when the acceptable media types are unspecified. */ public List<MediaType> getAccept() { return MediaType.parseMediaTypes(get(ACCEPT)); } /** * Set the acceptable language ranges, as specified by the * {@literal Accept-Language} header. * @since 5.0 */ public void setAcceptLanguage(List<Locale.LanguageRange> languages) { Assert.notNull(languages, "'languages' must not be null"); DecimalFormat decimal = new DecimalFormat("0.0", DECIMAL_FORMAT_SYMBOLS); List<String> values = languages.stream() .map(range -> range.getWeight() == Locale.LanguageRange.MAX_WEIGHT ? range.getRange() : range.getRange() + ";q=" + decimal.format(range.getWeight())) .collect(Collectors.toList()); set(ACCEPT_LANGUAGE, toCommaDelimitedString(values)); } /** * Return the language ranges from the {@literal "Accept-Language"} header. * <p>If you only need sorted, preferred locales only use * {@link #getAcceptLanguageAsLocales()} or if you need to filter based on * a list of supported locales you can pass the returned list to * {@link Locale#filter(List, Collection)}. * @since 5.0 */ public List<Locale.LanguageRange> getAcceptLanguage() { String value = getFirst(ACCEPT_LANGUAGE); return value != null ? Locale.LanguageRange.parse(value) : Collections.emptyList(); } /** * Variant of {@link #setAcceptLanguage(List)} using {@link Locale}'s. * @since 5.0 */ public void setAcceptLanguageAsLocales(List<Locale> locales) { setAcceptLanguage(locales.stream() .map(locale -> new Locale.LanguageRange(locale.toLanguageTag())) .collect(Collectors.toList())); } /** * A variant of {@link #getAcceptLanguage()} that converts each * {@link java.util.Locale.LanguageRange} to a {@link Locale}. * @return the locales or an empty list * @since 5.0 */ public List<Locale> getAcceptLanguageAsLocales() { List<Locale.LanguageRange> ranges = getAcceptLanguage(); if (ranges.isEmpty()) { return Collections.emptyList(); } return ranges.stream() .map(range -> Locale.forLanguageTag(range.getRange())) .filter(locale -> StringUtils.hasText(locale.getDisplayName())) .collect(Collectors.toList()); } /** * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header. */ public void setAccessControlAllowCredentials(boolean allowCredentials) { set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials)); } /** * Return the value of the {@code Access-Control-Allow-Credentials} response header. */ public boolean getAccessControlAllowCredentials() { return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); } /** * Set the (new) value of the {@code Access-Control-Allow-Headers} response header. */ public void setAccessControlAllowHeaders(List<String> allowedHeaders) { set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders)); } /** * Return the value of the {@code Access-Control-Allow-Headers} response header. */ public List<String> getAccessControlAllowHeaders() { return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS); } /** * Set the (new) value of the {@code Access-Control-Allow-Methods} response header. */ public void setAccessControlAllowMethods(List<HttpMethod> allowedMethods) { set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods)); } /** * Return the value of the {@code Access-Control-Allow-Methods} response header. */ public List<HttpMethod> getAccessControlAllowMethods() { List<HttpMethod> result = new ArrayList<>(); String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); if (value != null) { String[] tokens = StringUtils.tokenizeToStringArray(value, ","); for (String token : tokens) { HttpMethod resolved = HttpMethod.resolve(token); if (resolved != null) { result.add(resolved); } } } return result; } /** * Set the (new) value of the {@code Access-Control-Allow-Origin} response header. */ public void setAccessControlAllowOrigin(String allowedOrigin) { set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin); } /** * Return the value of the {@code Access-Control-Allow-Origin} response header. */ public String getAccessControlAllowOrigin() { return getFieldValues(ACCESS_CONTROL_ALLOW_ORIGIN); } /** * Set the (new) value of the {@code Access-Control-Expose-Headers} response header. */ public void setAccessControlExposeHeaders(List<String> exposedHeaders) { set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders)); } /** * Return the value of the {@code Access-Control-Expose-Headers} response header. */ public List<String> getAccessControlExposeHeaders() { return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS); } /** * Set the (new) value of the {@code Access-Control-Max-Age} response header. */ public void setAccessControlMaxAge(long maxAge) { set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge)); } /** * Return the value of the {@code Access-Control-Max-Age} response header. * <p>Returns -1 when the max age is unknown. */ public long getAccessControlMaxAge() { String value = getFirst(ACCESS_CONTROL_MAX_AGE); return (value != null ? Long.parseLong(value) : -1); } /** * Set the (new) value of the {@code Access-Control-Request-Headers} request header. */ public void setAccessControlRequestHeaders(List<String> requestHeaders) { set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders)); } /** * Return the value of the {@code Access-Control-Request-Headers} request header. */ public List<String> getAccessControlRequestHeaders() { return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS); } /** * Set the (new) value of the {@code Access-Control-Request-Method} request header. */ public void setAccessControlRequestMethod(HttpMethod requestMethod) { set(ACCESS_CONTROL_REQUEST_METHOD, requestMethod.name()); } /** * Return the value of the {@code Access-Control-Request-Method} request header. */ public HttpMethod getAccessControlRequestMethod() { return HttpMethod.resolve(getFirst(ACCESS_CONTROL_REQUEST_METHOD)); } /** * Set the list of acceptable {@linkplain Charset charsets}, * as specified by the {@code Accept-Charset} header. */ public void setAcceptCharset(List<Charset> acceptableCharsets) { StringBuilder builder = new StringBuilder(); for (Iterator<Charset> iterator = acceptableCharsets.iterator(); iterator.hasNext();) { Charset charset = iterator.next(); builder.append(charset.name().toLowerCase(Locale.ENGLISH)); if (iterator.hasNext()) { builder.append(", "); } } set(ACCEPT_CHARSET, builder.toString()); } /** * Return the list of acceptable {@linkplain Charset charsets}, * as specified by the {@code Accept-Charset} header. */ public List<Charset> getAcceptCharset() { String value = getFirst(ACCEPT_CHARSET); if (value != null) { String[] tokens = StringUtils.tokenizeToStringArray(value, ","); List<Charset> result = new ArrayList<>(tokens.length); for (String token : tokens) { int paramIdx = token.indexOf(';'); String charsetName; if (paramIdx == -1) { charsetName = token; } else { charsetName = token.substring(0, paramIdx); } if (!charsetName.equals("*")) { result.add(Charset.forName(charsetName)); } } return result; } else { return Collections.emptyList(); } } /** * Set the set of allowed {@link HttpMethod HTTP methods}, * as specified by the {@code Allow} header. */ public void setAllow(Set<HttpMethod> allowedMethods) { set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods)); } /** * Return the set of allowed {@link HttpMethod HTTP methods}, * as specified by the {@code Allow} header. * <p>Returns an empty set when the allowed methods are unspecified. */ public Set<HttpMethod> getAllow() { String value = getFirst(ALLOW); if (!StringUtils.isEmpty(value)) { String[] tokens = StringUtils.tokenizeToStringArray(value, ","); List<HttpMethod> result = new ArrayList<>(tokens.length); for (String token : tokens) { HttpMethod resolved = HttpMethod.resolve(token); if (resolved != null) { result.add(resolved); } } return EnumSet.copyOf(result); } else { return EnumSet.noneOf(HttpMethod.class); } } /** * Set the (new) value of the {@code Cache-Control} header. */ public void setCacheControl(String cacheControl) { set(CACHE_CONTROL, cacheControl); } /** * Return the value of the {@code Cache-Control} header. */ public String getCacheControl() { return getFieldValues(CACHE_CONTROL); } /** * Set the (new) value of the {@code Connection} header. */ public void setConnection(String connection) { set(CONNECTION, connection); } /** * Set the (new) value of the {@code Connection} header. */ public void setConnection(List<String> connection) { set(CONNECTION, toCommaDelimitedString(connection)); } /** * Return the value of the {@code Connection} header. */ public List<String> getConnection() { return getValuesAsList(CONNECTION); } /** * Set the (new) value of the {@code Content-Disposition} header * for {@code form-data}. * @param name the control name * @param filename the filename (may be {@code null}) * @see #setContentDisposition(ContentDisposition) * @see #getContentDisposition() */ public void setContentDispositionFormData(String name, String filename) { setContentDispositionFormData(name, filename, null); } /** * Set the (new) value of the {@code Content-Disposition} header * for {@code form-data}, optionally encoding the filename using the RFC 5987. * <p>Only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported. * @param name the control name * @param filename the filename (may be {@code null}) * @param charset the charset used for the filename (may be {@code null}) * @since 4.3.3 * @see #setContentDisposition(ContentDisposition) * @see #getContentDisposition() * @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.4">RFC 7230 Section 3.2.4</a> */ public void setContentDispositionFormData(String name, String filename, Charset charset) { Assert.notNull(name, "'name' must not be null"); ContentDisposition disposition = ContentDisposition.builder("form-data") .name(name).filename(filename, charset).build(); setContentDisposition(disposition); } /** * Set the (new) value of the {@literal Content-Disposition} header. Supports the * disposition type and {@literal filename}, {@literal filename*} (encoded according * to RFC 5987, only the US-ASCII, UTF-8 and ISO-8859-1 charsets are supported), * {@literal name}, {@literal size} parameters. * @since 5.0 * @see #getContentDisposition() */ public void setContentDisposition(ContentDisposition contentDisposition) { set(CONTENT_DISPOSITION, contentDisposition.toString()); } /** * Return the {@literal Content-Disposition} header parsed as a {@link ContentDisposition} * instance. Supports the disposition type and {@literal filename}, {@literal filename*} * (encoded according to RFC 5987, only the US-ASCII, UTF-8 and ISO-8859-1 charsets are * supported), {@literal name}, {@literal size} parameters. * @since 5.0 * @see #setContentDisposition(ContentDisposition) */ public ContentDisposition getContentDisposition() { String contentDisposition = getFirst(CONTENT_DISPOSITION); if (contentDisposition != null) { return ContentDisposition.parse(contentDisposition); } return ContentDisposition.empty(); } /** * Set the {@link Locale} of the content language, * as specified by the {@literal Content-Language} header. * <p>Use {@code set(CONTENT_LANGUAGE, ...)} if you need * to set multiple content languages.</p> * @since 5.0 */ public void setContentLanguage(Locale locale) { Assert.notNull(locale, "'locale' must not be null"); set(CONTENT_LANGUAGE, locale.toLanguageTag()); } /** * Return the first {@link Locale} of the content languages, * as specified by the {@literal Content-Language} header. * <p>Returns {@code null} when the content language is unknown. * <p>Use {@code getValuesAsList(CONTENT_LANGUAGE)} if you need * to get multiple content languages.</p> * @since 5.0 */ public Locale getContentLanguage() { return getValuesAsList(CONTENT_LANGUAGE) .stream() .findFirst() .map(Locale::forLanguageTag) .orElse(null); } /** * Set the length of the body in bytes, as specified by the * {@code Content-Length} header. */ public void setContentLength(long contentLength) { set(CONTENT_LENGTH, Long.toString(contentLength)); } /** * Return the length of the body in bytes, as specified by the * {@code Content-Length} header. * <p>Returns -1 when the content-length is unknown. */ public long getContentLength() { String value = getFirst(CONTENT_LENGTH); return (value != null ? Long.parseLong(value) : -1); } /** * Set the {@linkplain MediaType media type} of the body, * as specified by the {@code Content-Type} header. */ public void setContentType(MediaType mediaType) { Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); set(CONTENT_TYPE, mediaType.toString()); } /** * Return the {@linkplain MediaType media type} of the body, as specified * by the {@code Content-Type} header. * <p>Returns {@code null} when the content-type is unknown. */ public MediaType getContentType() { String value = getFirst(CONTENT_TYPE); return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null); } /** * Set the date and time at which the message was created, as specified * by the {@code Date} header. * <p>The date should be specified as the number of milliseconds since * January 1, 1970 GMT. */ public void setDate(long date) { setDate(DATE, date); } /** * Return the date and time at which the message was created, as specified * by the {@code Date} header. * <p>The date is returned as the number of milliseconds since * January 1, 1970 GMT. Returns -1 when the date is unknown. * @throws IllegalArgumentException if the value can't be converted to a date */ public long getDate() { return getFirstDate(DATE); } /** * Set the (new) entity tag of the body, as specified by the {@code ETag} header. */ public void setETag(String eTag) { if (eTag != null) { Assert.isTrue(eTag.startsWith("\"") || eTag.startsWith("W/"), "Invalid eTag, does not start with W/ or \""); Assert.isTrue(eTag.endsWith("\""), "Invalid eTag, does not end with \""); } set(ETAG, eTag); } /** * Return the entity tag of the body, as specified by the {@code ETag} header. */ public String getETag() { return getFirst(ETAG); } /** * Set the date and time at which the message is no longer valid, * as specified by the {@code Expires} header. * <p>The date should be specified as the number of milliseconds since * January 1, 1970 GMT. */ public void setExpires(long expires) { setDate(EXPIRES, expires); } /** * Return the date and time at which the message is no longer valid, * as specified by the {@code Expires} header. * <p>The date is returned as the number of milliseconds since * January 1, 1970 GMT. Returns -1 when the date is unknown. */ public long getExpires() { return getFirstDate(EXPIRES, false); } /** * Set the (new) value of the {@code Host} header. * <p>If the given {@linkplain InetSocketAddress#getPort() port} is {@code 0}, * the host header will only contain the * {@linkplain InetSocketAddress#getHostString() hostname}. * @since 5.0 */ public void setHost(InetSocketAddress host) { String value = (host.getPort() != 0 ? String.format("%s:%d", host.getHostString(), host.getPort()) : host.getHostString()); set(HOST, value); } /** * Return the value of the required {@code Host} header. * <p>If the header value does not contain a port, the returned * {@linkplain InetSocketAddress#getPort() port} will be {@code 0}. * @since 5.0 */ public InetSocketAddress getHost() { String value = getFirst(HOST); if (value == null) { return null; } int idx = value.lastIndexOf(':'); String hostname = null; int port = 0; if (idx != -1 && idx < value.length() - 1) { hostname = value.substring(0, idx); String portString = value.substring(idx + 1); try { port = Integer.parseInt(portString); } catch (NumberFormatException ex) { // ignored } } if (hostname == null) { hostname = value; } return InetSocketAddress.createUnresolved(hostname, port); } /** * 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. */ public void setIfModifiedSince(long ifModifiedSince) { setDate(IF_MODIFIED_SINCE, ifModifiedSince); } /** * Return the value of the {@code If-Modified-Since} header. * <p>The date is returned as the number of milliseconds since * January 1, 1970 GMT. Returns -1 when the date is unknown. */ public long getIfModifiedSince() { return getFirstDate(IF_MODIFIED_SINCE, false); } /** * Set the (new) value of the {@code If-None-Match} header. */ public void setIfNoneMatch(String ifNoneMatch) { set(IF_NONE_MATCH, ifNoneMatch); } /** * Set the (new) values of the {@code If-None-Match} header. */ public void setIfNoneMatch(List<String> ifNoneMatchList) { set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); } /** * Return the value of the {@code If-None-Match} header. */ public List<String> getIfNoneMatch() { return getETagValuesAsList(IF_NONE_MATCH); } /** * 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); } /** * Set the time the resource was last changed, as specified by the * {@code Last-Modified} header. * <p>The date should be specified as the number of milliseconds since * January 1, 1970 GMT. */ public void setLastModified(long lastModified) { setDate(LAST_MODIFIED, lastModified); } /** * Return the time the resource was last changed, as specified by the * {@code Last-Modified} header. * <p>The date is returned as the number of milliseconds since * January 1, 1970 GMT. Returns -1 when the date is unknown. */ public long getLastModified() { return getFirstDate(LAST_MODIFIED, false); } /** * Set the (new) location of a resource, * as specified by the {@code Location} header. */ public void setLocation(URI location) { set(LOCATION, location.toASCIIString()); } /** * Return the (new) location of a resource * as specified by the {@code Location} header. * <p>Returns {@code null} when the location is unknown. */ public URI getLocation() { String value = getFirst(LOCATION); return (value != null ? URI.create(value) : null); } /** * Set the (new) value of the {@code Origin} header. */ public void setOrigin(String origin) { set(ORIGIN, origin); } /** * Return the value of the {@code Origin} header. */ public String getOrigin() { return getFirst(ORIGIN); } /** * Set the (new) value of the {@code Pragma} header. */ public void setPragma(String pragma) { set(PRAGMA, pragma); } /** * Return the value of the {@code Pragma} header. */ public String getPragma() { return getFirst(PRAGMA); } /** * Sets the (new) value of the {@code Range} header. */ public void setRange(List<HttpRange> ranges) { String value = HttpRange.toString(ranges); set(RANGE, value); } /** * Return the value of the {@code Range} header. * <p>Returns an empty list when the range is unknown. */ public List<HttpRange> getRange() { String value = getFirst(RANGE); return HttpRange.parseRanges(value); } /** * Set the (new) value of the {@code Upgrade} header. */ public void setUpgrade(String upgrade) { set(UPGRADE, upgrade); } /** * Return the value of the {@code Upgrade} header. */ public String getUpgrade() { return getFirst(UPGRADE); } /** * 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); } /** * Parse the first header value for the given header name as a date, * return -1 if there is no value or also in case of an invalid value * (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException} * if the value cannot be parsed as a date. * @param headerName the header name * @param rejectInvalid whether to reject invalid values with an * {@link IllegalArgumentException} ({@code true}) or rather return -1 * in that case ({@code false}) * @return the parsed date header, or -1 if none (or invalid) */ private long getFirstDate(String headerName, boolean rejectInvalid) { String headerValue = getFirst(headerName); if (headerValue == null) { // No header value sent at all return -1; } if (headerValue.length() >= 3) { // Short "0" or "-1" like values are never valid HTTP date headers... // Let's only bother with SimpleDateFormat parsing for long enough values. for (String dateFormat : DATE_FORMATS) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); simpleDateFormat.setTimeZone(GMT); try { return simpleDateFormat.parse(headerValue).getTime(); } catch (ParseException ex) { // ignore } } } if (rejectInvalid) { throw new IllegalArgumentException("Cannot parse date value \"" + headerValue + "\" for \"" + headerName + "\" header"); } return -1; } /** * 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<>(); 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<>(); 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 */ 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 * @return the first header value, or {@code null} if none */ @Override public String getFirst(String headerName) { List<String> headerValues = this.headers.get(headerName); return (headerValues != null ? headerValues.get(0) : null); } /** * Add the given, single header value under the given name. * @param headerName the header name * @param headerValue the header value * @throws UnsupportedOperationException if adding headers is not supported * @see #put(String, List) * @see #set(String, String) */ @Override public void add(String headerName, String headerValue) { List<String> headerValues = this.headers.computeIfAbsent(headerName, k -> new LinkedList<>()); headerValues.add(headerValue); } @Override public void addAll(String key, List<String> values) { List<String> currentValues = this.headers.computeIfAbsent(key, k -> new LinkedList<>()); currentValues.addAll(values); } /** * Set the given, single header value under the given name. * @param headerName the header name * @param headerValue the header value * @throws UnsupportedOperationException if adding headers is not supported * @see #put(String, List) * @see #add(String, String) */ @Override public void set(String headerName, String headerValue) { List<String> headerValues = new LinkedList<>(); headerValues.add(headerValue); this.headers.put(headerName, headerValues); } @Override public void setAll(Map<String, String> values) { for (Entry<String, String> entry : values.entrySet()) { set(entry.getKey(), entry.getValue()); } } @Override public Map<String, String> toSingleValueMap() { LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<>(this.headers.size()); for (Entry<String, List<String>> entry : this.headers.entrySet()) { singleValueMap.put(entry.getKey(), entry.getValue().get(0)); } return singleValueMap; } // Map implementation @Override public int size() { return this.headers.size(); } @Override public boolean isEmpty() { return this.headers.isEmpty(); } @Override public boolean containsKey(Object key) { return this.headers.containsKey(key); } @Override public boolean containsValue(Object value) { return this.headers.containsValue(value); } @Override public List<String> get(Object key) { return this.headers.get(key); } @Override public List<String> put(String key, List<String> value) { return this.headers.put(key, value); } @Override public List<String> remove(Object key) { return this.headers.remove(key); } @Override public void putAll(Map<? extends String, ? extends List<String>> map) { this.headers.putAll(map); } @Override public void clear() { this.headers.clear(); } @Override public Set<String> keySet() { return this.headers.keySet(); } @Override public Collection<List<String>> values() { return this.headers.values(); } @Override public Set<Entry<String, List<String>>> entrySet() { return this.headers.entrySet(); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof HttpHeaders)) { return false; } HttpHeaders otherHeaders = (HttpHeaders) other; return this.headers.equals(otherHeaders.headers); } @Override public int hashCode() { return this.headers.hashCode(); } @Override public String toString() { return this.headers.toString(); } /** * Return a {@code HttpHeaders} object that can only be read, not written to. */ public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { return new HttpHeaders(headers, true); } }