org.springframework.http.HttpHeaders Maven / Gradle / Ivy
/*
* Copyright 2002-2024 the original author 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
*
* https://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.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* A data structure representing HTTP request or response headers, mapping String header names
* to a list of String values, also offering accessors for common application-level data types.
*
* In addition to the regular methods defined by {@link Map}, this class offers many common
* convenience methods, for example:
*
* - {@link #getFirst(String)} returns the first value associated with a given header name
* - {@link #add(String, String)} adds a header value to the list of values for a header name
* - {@link #set(String, String)} sets the header value to a single string value
*
*
* Note that {@code HttpHeaders} instances created by the default constructor
* treat header names in a case-insensitive manner. Instances created with the
* {@link #HttpHeaders(MultiValueMap)} constructor like those instantiated
* internally by the framework to adapt to existing HTTP headers data structures
* do guarantee per-header get/set/add operations to be case-insensitive as
* mandated by the HTTP specification. However, it is not necessarily the case
* for operations that deal with the collection as a whole (like {@code size()},
* {@code values()}, {@code keySet()} and {@code entrySet()}). Prefer using
* {@link #headerSet()} for these cases.
*
*
Some backing implementations can store header names in a case-sensitive
* manner, which will lead to duplicates during the entrySet() iteration where
* multiple occurrences of a header name can surface depending on letter casing
* but each such entry has the full {@code List} of values. — This can be
* problematic for example when copying headers into a new instance by iterating
* over the old instance's {@code entrySet()} and using
* {@link #addAll(String, List)} rather than {@link #put(String, List)}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Brian Clozel
* @author Juergen Hoeller
* @author Josh Long
* @author Sam Brannen
* @author Simon Baslé
* @since 3.0
*/
public class HttpHeaders implements MultiValueMap, Serializable {
private static final long serialVersionUID = -8578554704772377436L;
/**
* The HTTP {@code Accept} header field name.
* @see Section 5.3.2 of RFC 7231
*/
public static final String ACCEPT = "Accept";
/**
* The HTTP {@code Accept-Charset} header field name.
* @see Section 5.3.3 of RFC 7231
*/
public static final String ACCEPT_CHARSET = "Accept-Charset";
/**
* The HTTP {@code Accept-Encoding} header field name.
* @see Section 5.3.4 of RFC 7231
*/
public static final String ACCEPT_ENCODING = "Accept-Encoding";
/**
* The HTTP {@code Accept-Language} header field name.
* @see Section 5.3.5 of RFC 7231
*/
public static final String ACCEPT_LANGUAGE = "Accept-Language";
/**
* The HTTP {@code Accept-Patch} header field name.
* @since 5.3.6
* @see Section 3.1 of RFC 5789
*/
public static final String ACCEPT_PATCH = "Accept-Patch";
/**
* The HTTP {@code Accept-Ranges} header field name.
* @see Section 5.3.5 of RFC 7233
*/
public static final String ACCEPT_RANGES = "Accept-Ranges";
/**
* The CORS {@code Access-Control-Allow-Credentials} response header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
/**
* The CORS {@code Access-Control-Allow-Headers} response header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
/**
* The CORS {@code Access-Control-Allow-Methods} response header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
/**
* The CORS {@code Access-Control-Allow-Origin} response header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
/**
* The CORS {@code Access-Control-Expose-Headers} response header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
/**
* The CORS {@code Access-Control-Max-Age} response header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
/**
* The CORS {@code Access-Control-Request-Headers} request header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
/**
* The CORS {@code Access-Control-Request-Method} request header field name.
* @see CORS W3C recommendation
*/
public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
/**
* The HTTP {@code Age} header field name.
* @see Section 5.1 of RFC 7234
*/
public static final String AGE = "Age";
/**
* The HTTP {@code Allow} header field name.
* @see Section 7.4.1 of RFC 7231
*/
public static final String ALLOW = "Allow";
/**
* The HTTP {@code Authorization} header field name.
* @see Section 4.2 of RFC 7235
*/
public static final String AUTHORIZATION = "Authorization";
/**
* The HTTP {@code Cache-Control} header field name.
* @see Section 5.2 of RFC 7234
*/
public static final String CACHE_CONTROL = "Cache-Control";
/**
* The HTTP {@code Connection} header field name.
* @see Section 6.1 of RFC 7230
*/
public static final String CONNECTION = "Connection";
/**
* The HTTP {@code Content-Encoding} header field name.
* @see Section 3.1.2.2 of RFC 7231
*/
public static final String CONTENT_ENCODING = "Content-Encoding";
/**
* The HTTP {@code Content-Disposition} header field name.
* @see RFC 6266
*/
public static final String CONTENT_DISPOSITION = "Content-Disposition";
/**
* The HTTP {@code Content-Language} header field name.
* @see Section 3.1.3.2 of RFC 7231
*/
public static final String CONTENT_LANGUAGE = "Content-Language";
/**
* The HTTP {@code Content-Length} header field name.
* @see Section 3.3.2 of RFC 7230
*/
public static final String CONTENT_LENGTH = "Content-Length";
/**
* The HTTP {@code Content-Location} header field name.
* @see Section 3.1.4.2 of RFC 7231
*/
public static final String CONTENT_LOCATION = "Content-Location";
/**
* The HTTP {@code Content-Range} header field name.
* @see Section 4.2 of RFC 7233
*/
public static final String CONTENT_RANGE = "Content-Range";
/**
* The HTTP {@code Content-Type} header field name.
* @see Section 3.1.1.5 of RFC 7231
*/
public static final String CONTENT_TYPE = "Content-Type";
/**
* The HTTP {@code Cookie} header field name.
* @see Section 4.3.4 of RFC 2109
*/
public static final String COOKIE = "Cookie";
/**
* The HTTP {@code Date} header field name.
* @see Section 7.1.1.2 of RFC 7231
*/
public static final String DATE = "Date";
/**
* The HTTP {@code ETag} header field name.
* @see Section 2.3 of RFC 7232
*/
public static final String ETAG = "ETag";
/**
* The HTTP {@code Expect} header field name.
* @see Section 5.1.1 of RFC 7231
*/
public static final String EXPECT = "Expect";
/**
* The HTTP {@code Expires} header field name.
* @see Section 5.3 of RFC 7234
*/
public static final String EXPIRES = "Expires";
/**
* The HTTP {@code From} header field name.
* @see Section 5.5.1 of RFC 7231
*/
public static final String FROM = "From";
/**
* The HTTP {@code Host} header field name.
* @see Section 5.4 of RFC 7230
*/
public static final String HOST = "Host";
/**
* The HTTP {@code If-Match} header field name.
* @see Section 3.1 of RFC 7232
*/
public static final String IF_MATCH = "If-Match";
/**
* The HTTP {@code If-Modified-Since} header field name.
* @see Section 3.3 of RFC 7232
*/
public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
/**
* The HTTP {@code If-None-Match} header field name.
* @see Section 3.2 of RFC 7232
*/
public static final String IF_NONE_MATCH = "If-None-Match";
/**
* The HTTP {@code If-Range} header field name.
* @see Section 3.2 of RFC 7233
*/
public static final String IF_RANGE = "If-Range";
/**
* The HTTP {@code If-Unmodified-Since} header field name.
* @see Section 3.4 of RFC 7232
*/
public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
/**
* The HTTP {@code Last-Modified} header field name.
* @see Section 2.2 of RFC 7232
*/
public static final String LAST_MODIFIED = "Last-Modified";
/**
* The HTTP {@code Link} header field name.
* @see RFC 5988
*/
public static final String LINK = "Link";
/**
* The HTTP {@code Location} header field name.
* @see Section 7.1.2 of RFC 7231
*/
public static final String LOCATION = "Location";
/**
* The HTTP {@code Max-Forwards} header field name.
* @see Section 5.1.2 of RFC 7231
*/
public static final String MAX_FORWARDS = "Max-Forwards";
/**
* The HTTP {@code Origin} header field name.
* @see RFC 6454
*/
public static final String ORIGIN = "Origin";
/**
* The HTTP {@code Pragma} header field name.
* @see Section 5.4 of RFC 7234
*/
public static final String PRAGMA = "Pragma";
/**
* The HTTP {@code Proxy-Authenticate} header field name.
* @see Section 4.3 of RFC 7235
*/
public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
/**
* The HTTP {@code Proxy-Authorization} header field name.
* @see Section 4.4 of RFC 7235
*/
public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
/**
* The HTTP {@code Range} header field name.
* @see Section 3.1 of RFC 7233
*/
public static final String RANGE = "Range";
/**
* The HTTP {@code Referer} header field name.
* @see Section 5.5.2 of RFC 7231
*/
public static final String REFERER = "Referer";
/**
* The HTTP {@code Retry-After} header field name.
* @see Section 7.1.3 of RFC 7231
*/
public static final String RETRY_AFTER = "Retry-After";
/**
* The HTTP {@code Server} header field name.
* @see Section 7.4.2 of RFC 7231
*/
public static final String SERVER = "Server";
/**
* The HTTP {@code Set-Cookie} header field name.
* @see Section 4.2.2 of RFC 2109
*/
public static final String SET_COOKIE = "Set-Cookie";
/**
* The HTTP {@code Set-Cookie2} header field name.
* @see RFC 2965
*/
public static final String SET_COOKIE2 = "Set-Cookie2";
/**
* The HTTP {@code TE} header field name.
* @see Section 4.3 of RFC 7230
*/
public static final String TE = "TE";
/**
* The HTTP {@code Trailer} header field name.
* @see Section 4.4 of RFC 7230
*/
public static final String TRAILER = "Trailer";
/**
* The HTTP {@code Transfer-Encoding} header field name.
* @see Section 3.3.1 of RFC 7230
*/
public static final String TRANSFER_ENCODING = "Transfer-Encoding";
/**
* The HTTP {@code Upgrade} header field name.
* @see Section 6.7 of RFC 7230
*/
public static final String UPGRADE = "Upgrade";
/**
* The HTTP {@code User-Agent} header field name.
* @see Section 5.5.3 of RFC 7231
*/
public static final String USER_AGENT = "User-Agent";
/**
* The HTTP {@code Vary} header field name.
* @see Section 7.1.4 of RFC 7231
*/
public static final String VARY = "Vary";
/**
* The HTTP {@code Via} header field name.
* @see Section 5.7.1 of RFC 7230
*/
public static final String VIA = "Via";
/**
* The HTTP {@code Warning} header field name.
* @see Section 5.5 of RFC 7234
*/
public static final String WARNING = "Warning";
/**
* The HTTP {@code WWW-Authenticate} header field name.
* @see Section 4.1 of RFC 7235
*/
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
/**
* An empty {@code HttpHeaders} instance (immutable).
* @since 5.0
*/
public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>());
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ROOT);
private static final ZoneId GMT = ZoneId.of("GMT");
/**
* Date formats with time zone as specified in the HTTP RFC to use for formatting.
* @see Section 7.1.1.1 of RFC 7231
*/
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US).withZone(GMT);
/**
* Date formats with time zone as specified in the HTTP RFC to use for parsing.
* @see Section 7.1.1.1 of RFC 7231
*/
private static final DateTimeFormatter[] DATE_PARSERS = new DateTimeFormatter[] {
DateTimeFormatter.RFC_1123_DATE_TIME,
DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(GMT)
};
@SuppressWarnings("serial")
final MultiValueMap headers;
/**
* Construct a new, empty instance of the {@code HttpHeaders} object
* using an underlying case-insensitive map.
*/
public HttpHeaders() {
this(CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ROOT)));
}
/**
* Construct a new {@code HttpHeaders} instance backed by an existing map.
* This constructor is available as an optimization for adapting to existing
* headers map structures, primarily for internal use within the framework.
* @param headers the headers map (expected to operate with case-insensitive keys)
* @since 5.1
*/
public HttpHeaders(MultiValueMap headers) {
Assert.notNull(headers, "MultiValueMap must not be null");
if (headers == EMPTY) {
this.headers = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
}
else if (headers instanceof HttpHeaders httpHeaders) {
while (httpHeaders.headers instanceof HttpHeaders wrapped) {
httpHeaders = wrapped;
}
this.headers = httpHeaders.headers;
}
else {
this.headers = headers;
}
}
/**
* Get the list of header values for the given header name, if any.
* @param headerName the header name
* @return the list of header values, or an empty list
* @since 5.2
*/
public List getOrEmpty(Object headerName) {
List values = get(headerName);
return (values != null ? values : Collections.emptyList());
}
/**
* Set the list of acceptable {@linkplain MediaType media types},
* as specified by the {@code Accept} header.
*/
public void setAccept(List acceptableMediaTypes) {
set(ACCEPT, MediaType.toString(acceptableMediaTypes));
}
/**
* Return the list of acceptable {@linkplain MediaType media types},
* as specified by the {@code Accept} header.
* Returns an empty list when the acceptable media types are unspecified.
*/
public List 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 languages) {
Assert.notNull(languages, "LanguageRange List must not be null");
DecimalFormat decimal = new DecimalFormat("0.0", DECIMAL_FORMAT_SYMBOLS);
List values = languages.stream()
.map(range ->
range.getWeight() == Locale.LanguageRange.MAX_WEIGHT ?
range.getRange() :
range.getRange() + ";q=" + decimal.format(range.getWeight()))
.toList();
set(ACCEPT_LANGUAGE, toCommaDelimitedString(values));
}
/**
* Return the language ranges from the {@literal "Accept-Language"} header.
* 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)}.
* @throws IllegalArgumentException if the value cannot be converted to a language range
* @since 5.0
*/
public List getAcceptLanguage() {
String value = getFirst(ACCEPT_LANGUAGE);
if (StringUtils.hasText(value)) {
try {
return Locale.LanguageRange.parse(value);
}
catch (IllegalArgumentException ignored) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
for (int i = 0; i < tokens.length; i++) {
tokens[i] = StringUtils.trimTrailingCharacter(tokens[i], ';');
}
value = StringUtils.arrayToCommaDelimitedString(tokens);
return Locale.LanguageRange.parse(value);
}
}
return Collections.emptyList();
}
/**
* Variant of {@link #setAcceptLanguage(List)} using {@link Locale}'s.
* @since 5.0
*/
public void setAcceptLanguageAsLocales(List locales) {
setAcceptLanguage(locales.stream()
.map(locale -> new Locale.LanguageRange(locale.toLanguageTag()))
.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
* @throws IllegalArgumentException if the value cannot be converted to a locale
* @since 5.0
*/
public List getAcceptLanguageAsLocales() {
List ranges = getAcceptLanguage();
if (ranges.isEmpty()) {
return Collections.emptyList();
}
List locales = new ArrayList<>(ranges.size());
for (Locale.LanguageRange range : ranges) {
if (!range.getRange().startsWith("*")) {
locales.add(Locale.forLanguageTag(range.getRange()));
}
}
return locales;
}
/**
* Set the list of acceptable {@linkplain MediaType media types} for
* {@code PATCH} methods, as specified by the {@code Accept-Patch} header.
* @since 5.3.6
*/
public void setAcceptPatch(List mediaTypes) {
set(ACCEPT_PATCH, MediaType.toString(mediaTypes));
}
/**
* Return the list of acceptable {@linkplain MediaType media types} for
* {@code PATCH} methods, as specified by the {@code Accept-Patch} header.
* Returns an empty list when the acceptable media types are unspecified.
* @since 5.3.6
*/
public List getAcceptPatch() {
return MediaType.parseMediaTypes(get(ACCEPT_PATCH));
}
/**
* 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 allowedHeaders) {
set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders));
}
/**
* Return the value of the {@code Access-Control-Allow-Headers} response header.
*/
public List getAccessControlAllowHeaders() {
return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS);
}
/**
* Set the (new) value of the {@code Access-Control-Allow-Methods} response header.
*/
public void setAccessControlAllowMethods(List allowedMethods) {
set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods));
}
/**
* Return the value of the {@code Access-Control-Allow-Methods} response header.
*/
public List getAccessControlAllowMethods() {
String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS);
if (value != null) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
List result = new ArrayList<>(tokens.length);
for (String token : tokens) {
HttpMethod method = HttpMethod.valueOf(token);
result.add(method);
}
return result;
}
else {
return Collections.emptyList();
}
}
/**
* Set the (new) value of the {@code Access-Control-Allow-Origin} response header.
*/
public void setAccessControlAllowOrigin(@Nullable String allowedOrigin) {
setOrRemove(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin);
}
/**
* Return the value of the {@code Access-Control-Allow-Origin} response header.
*/
@Nullable
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 exposedHeaders) {
set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders));
}
/**
* Return the value of the {@code Access-Control-Expose-Headers} response header.
*/
public List getAccessControlExposeHeaders() {
return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS);
}
/**
* Set the (new) value of the {@code Access-Control-Max-Age} response header.
* @since 5.2
*/
public void setAccessControlMaxAge(Duration maxAge) {
set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge.getSeconds()));
}
/**
* 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.
* 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 requestHeaders) {
set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders));
}
/**
* Return the value of the {@code Access-Control-Request-Headers} request header.
*/
public List getAccessControlRequestHeaders() {
return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS);
}
/**
* Set the (new) value of the {@code Access-Control-Request-Method} request header.
*/
public void setAccessControlRequestMethod(@Nullable HttpMethod requestMethod) {
setOrRemove(ACCESS_CONTROL_REQUEST_METHOD, (requestMethod != null ? requestMethod.name() : null));
}
/**
* Return the value of the {@code Access-Control-Request-Method} request header.
*/
@Nullable
public HttpMethod getAccessControlRequestMethod() {
String requestMethod = getFirst(ACCESS_CONTROL_REQUEST_METHOD);
if (requestMethod != null) {
return HttpMethod.valueOf(requestMethod);
}
else {
return null;
}
}
/**
* Set the list of acceptable {@linkplain Charset charsets},
* as specified by the {@code Accept-Charset} header.
*/
public void setAcceptCharset(List acceptableCharsets) {
StringJoiner joiner = new StringJoiner(", ");
for (Charset charset : acceptableCharsets) {
joiner.add(charset.name().toLowerCase(Locale.ROOT));
}
set(ACCEPT_CHARSET, joiner.toString());
}
/**
* Return the list of acceptable {@linkplain Charset charsets},
* as specified by the {@code Accept-Charset} header.
*/
public List getAcceptCharset() {
String value = getFirst(ACCEPT_CHARSET);
if (value != null) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
List 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 allowedMethods) {
set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods));
}
/**
* Return the set of allowed {@link HttpMethod HTTP methods},
* as specified by the {@code Allow} header.
* Returns an empty set when the allowed methods are unspecified.
*/
public Set getAllow() {
String value = getFirst(ALLOW);
if (StringUtils.hasLength(value)) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
Set result = CollectionUtils.newLinkedHashSet(tokens.length);
for (String token : tokens) {
HttpMethod method = HttpMethod.valueOf(token);
result.add(method);
}
return result;
}
else {
return Collections.emptySet();
}
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* Basic Authentication based on the given username and password.
* Note that this method only supports characters in the
* {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
* @param username the username
* @param password the password
* @throws IllegalArgumentException if either {@code user} or
* {@code password} contain characters that cannot be encoded to ISO-8859-1
* @since 5.1
* @see #setBasicAuth(String)
* @see #setBasicAuth(String, String, Charset)
* @see #encodeBasicAuth(String, String, Charset)
* @see RFC 7617
*/
public void setBasicAuth(String username, String password) {
setBasicAuth(username, password, null);
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* Basic Authentication based on the given username and password.
* @param username the username
* @param password the password
* @param charset the charset to use to convert the credentials into an octet
* sequence. Defaults to {@linkplain StandardCharsets#ISO_8859_1 ISO-8859-1}.
* @throws IllegalArgumentException if {@code username} or {@code password}
* contains characters that cannot be encoded to the given charset
* @since 5.1
* @see #setBasicAuth(String)
* @see #setBasicAuth(String, String)
* @see #encodeBasicAuth(String, String, Charset)
* @see RFC 7617
*/
public void setBasicAuth(String username, String password, @Nullable Charset charset) {
setBasicAuth(encodeBasicAuth(username, password, charset));
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* Basic Authentication based on the given {@linkplain #encodeBasicAuth
* encoded credentials}.
*
Favor this method over {@link #setBasicAuth(String, String)} and
* {@link #setBasicAuth(String, String, Charset)} if you wish to cache the
* encoded credentials.
* @param encodedCredentials the encoded credentials
* @throws IllegalArgumentException if supplied credentials string is
* {@code null} or blank
* @since 5.2
* @see #setBasicAuth(String, String)
* @see #setBasicAuth(String, String, Charset)
* @see #encodeBasicAuth(String, String, Charset)
* @see RFC 7617
*/
public void setBasicAuth(String encodedCredentials) {
Assert.hasText(encodedCredentials, "'encodedCredentials' must not be null or blank");
set(AUTHORIZATION, "Basic " + encodedCredentials);
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* the given Bearer token.
* @param token the Base64 encoded token
* @since 5.1
* @see RFC 6750
*/
public void setBearerAuth(String token) {
set(AUTHORIZATION, "Bearer " + token);
}
/**
* Set a configured {@link CacheControl} instance as the
* new value of the {@code Cache-Control} header.
* @since 5.0.5
*/
public void setCacheControl(CacheControl cacheControl) {
setOrRemove(CACHE_CONTROL, cacheControl.getHeaderValue());
}
/**
* Set the (new) value of the {@code Cache-Control} header.
*/
public void setCacheControl(@Nullable String cacheControl) {
setOrRemove(CACHE_CONTROL, cacheControl);
}
/**
* Return the value of the {@code Cache-Control} header.
*/
@Nullable
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 connection) {
set(CONNECTION, toCommaDelimitedString(connection));
}
/**
* Return the value of the {@code Connection} header.
*/
public List getConnection() {
return getValuesAsList(CONNECTION);
}
/**
* Set the {@code Content-Disposition} header when creating a
* {@code "multipart/form-data"} request.
* Applications typically would not set this header directly but
* rather prepare a {@code MultiValueMap}, containing an
* Object or a {@link org.springframework.core.io.Resource} for each part,
* and then pass that to the {@code RestTemplate} or {@code WebClient}.
* @param name the control name
* @param filename the filename (may be {@code null})
* @see #getContentDisposition()
*/
public void setContentDispositionFormData(String name, @Nullable String filename) {
Assert.notNull(name, "Name must not be null");
ContentDisposition.Builder disposition = ContentDisposition.formData().name(name);
if (StringUtils.hasText(filename)) {
disposition.filename(filename);
}
setContentDisposition(disposition.build());
}
/**
* Set the {@literal Content-Disposition} header.
* This could be used on a response to indicate if the content is
* expected to be displayed inline in the browser or as an attachment to be
* saved locally.
*
It can also be used for a {@code "multipart/form-data"} request.
* For more details see notes on {@link #setContentDispositionFormData}.
* @since 5.0
* @see #getContentDisposition()
*/
public void setContentDisposition(ContentDisposition contentDisposition) {
set(CONTENT_DISPOSITION, contentDisposition.toString());
}
/**
* Return a parsed representation of the {@literal Content-Disposition} header.
* @since 5.0
* @see #setContentDisposition(ContentDisposition)
*/
public ContentDisposition getContentDisposition() {
String contentDisposition = getFirst(CONTENT_DISPOSITION);
if (StringUtils.hasText(contentDisposition)) {
return ContentDisposition.parse(contentDisposition);
}
return ContentDisposition.empty();
}
/**
* Set the {@link Locale} of the content language,
* as specified by the {@literal Content-Language} header.
*
Use {@code put(CONTENT_LANGUAGE, list)} if you need
* to set multiple content languages.
* @since 5.0
*/
public void setContentLanguage(@Nullable Locale locale) {
setOrRemove(CONTENT_LANGUAGE, (locale != null ? locale.toLanguageTag() : null));
}
/**
* Get the first {@link Locale} of the content languages, as specified by the
* {@code Content-Language} header.
* Use {@link #getValuesAsList(String)} if you need to get multiple content
* languages.
* @return the first {@code Locale} of the content languages, or {@code null}
* if unknown
* @since 5.0
*/
@Nullable
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.
* @param contentLength content length (greater than or equal to zero)
* @throws IllegalArgumentException if the content length is negative
*/
public void setContentLength(long contentLength) {
if (contentLength < 0) {
throw new IllegalArgumentException("Content-Length must be a non-negative number");
}
set(CONTENT_LENGTH, Long.toString(contentLength));
}
/**
* Return the length of the body in bytes, as specified by the
* {@code Content-Length} header.
*
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(@Nullable MediaType mediaType) {
if (mediaType != null) {
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());
}
else {
remove(CONTENT_TYPE);
}
}
/**
* Return the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
*
Returns {@code null} when the {@code Content-Type} header is not set.
* @throws InvalidMediaTypeException if the media type value cannot be parsed
*/
@Nullable
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.
* @since 5.2
*/
public void setDate(ZonedDateTime date) {
setZonedDateTime(DATE, date);
}
/**
* Set the date and time at which the message was created, as specified
* by the {@code Date} header.
* @since 5.2
*/
public void setDate(Instant date) {
setInstant(DATE, date);
}
/**
* Set the date and time at which the message was created, as specified
* by the {@code Date} header.
*
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.
*
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 cannot 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(@Nullable String tag) {
if (tag != null) {
set(ETAG, ETag.quoteETagIfNecessary(tag));
}
else {
remove(ETAG);
}
}
/**
* Return the entity tag of the body, as specified by the {@code ETag} header.
*/
@Nullable
public String getETag() {
return getFirst(ETAG);
}
/**
* Set the duration after which the message is no longer valid,
* as specified by the {@code Expires} header.
* @since 5.0.5
*/
public void setExpires(ZonedDateTime expires) {
setZonedDateTime(EXPIRES, expires);
}
/**
* Set the date and time at which the message is no longer valid,
* as specified by the {@code Expires} header.
* @since 5.2
*/
public void setExpires(Instant expires) {
setInstant(EXPIRES, expires);
}
/**
* Set the date and time at which the message is no longer valid,
* as specified by the {@code Expires} header.
*
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.
*
The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @see #getFirstZonedDateTime(String)
*/
public long getExpires() {
return getFirstDate(EXPIRES, false);
}
/**
* Set the (new) value of the {@code Host} header.
*
If the given {@linkplain InetSocketAddress#getPort() port} is {@code 0},
* the host header will only contain the
* {@linkplain InetSocketAddress#getHostString() host name}.
* @since 5.0
*/
public void setHost(@Nullable InetSocketAddress host) {
if (host != null) {
String value = host.getHostString();
int port = host.getPort();
if (port != 0) {
value = value + ":" + port;
}
set(HOST, value);
}
else {
remove(HOST);
}
}
/**
* Return the value of the {@code Host} header, if available.
*
If the header value does not contain a port, the
* {@linkplain InetSocketAddress#getPort() port} in the returned address will
* be {@code 0}.
* @since 5.0
*/
@Nullable
public InetSocketAddress getHost() {
String value = getFirst(HOST);
if (value == null) {
return null;
}
String host = null;
int port = 0;
int separator = (value.startsWith("[") ? value.indexOf(':', value.indexOf(']')) : value.lastIndexOf(':'));
if (separator != -1) {
host = value.substring(0, separator);
String portString = value.substring(separator + 1);
try {
port = Integer.parseInt(portString);
}
catch (NumberFormatException ex) {
// ignore
}
}
if (host == null) {
host = value;
}
return InetSocketAddress.createUnresolved(host, 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 ifMatchList) {
set(IF_MATCH, toCommaDelimitedString(ifMatchList));
}
/**
* Return the value of the {@code If-Match} header.
* @throws IllegalArgumentException if parsing fails
* @since 4.3
*/
public List getIfMatch() {
return getETagValuesAsList(IF_MATCH);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfModifiedSince(ZonedDateTime ifModifiedSince) {
setZonedDateTime(IF_MODIFIED_SINCE, ifModifiedSince.withZoneSameInstant(GMT));
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfModifiedSince(Instant ifModifiedSince) {
setInstant(IF_MODIFIED_SINCE, ifModifiedSince);
}
/**
* Set the (new) value of the {@code If-Modified-Since} header.
* 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.
*
The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @see #getFirstZonedDateTime(String)
*/
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 ifNoneMatchList) {
set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList));
}
/**
* Return the value of the {@code If-None-Match} header.
* @throws IllegalArgumentException if parsing fails
*/
public List getIfNoneMatch() {
return getETagValuesAsList(IF_NONE_MATCH);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfUnmodifiedSince(ZonedDateTime ifUnmodifiedSince) {
setZonedDateTime(IF_UNMODIFIED_SINCE, ifUnmodifiedSince.withZoneSameInstant(GMT));
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfUnmodifiedSince(Instant ifUnmodifiedSince) {
setInstant(IF_UNMODIFIED_SINCE, ifUnmodifiedSince);
}
/**
* Set the (new) value of the {@code If-Unmodified-Since} header.
* 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.
*
The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @since 4.3
* @see #getFirstZonedDateTime(String)
*/
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.
* @since 5.1.4
*/
public void setLastModified(ZonedDateTime lastModified) {
setZonedDateTime(LAST_MODIFIED, lastModified.withZoneSameInstant(GMT));
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setLastModified(Instant lastModified) {
setInstant(LAST_MODIFIED, lastModified);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
*
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.
*
The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @see #getFirstZonedDateTime(String)
*/
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(@Nullable URI location) {
setOrRemove(LOCATION, (location != null ? location.toASCIIString() : null));
}
/**
* Return the (new) location of a resource
* as specified by the {@code Location} header.
*
Returns {@code null} when the location is unknown.
*/
@Nullable
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(@Nullable String origin) {
setOrRemove(ORIGIN, origin);
}
/**
* Return the value of the {@code Origin} header.
*/
@Nullable
public String getOrigin() {
return getFirst(ORIGIN);
}
/**
* Set the (new) value of the {@code Pragma} header.
*/
public void setPragma(@Nullable String pragma) {
setOrRemove(PRAGMA, pragma);
}
/**
* Return the value of the {@code Pragma} header.
*/
@Nullable
public String getPragma() {
return getFirst(PRAGMA);
}
/**
* Sets the (new) value of the {@code Range} header.
*/
public void setRange(List ranges) {
String value = HttpRange.toString(ranges);
set(RANGE, value);
}
/**
* Return the value of the {@code Range} header.
* Returns an empty list when the range is unknown.
*/
public List getRange() {
String value = getFirst(RANGE);
return HttpRange.parseRanges(value);
}
/**
* Set the (new) value of the {@code Upgrade} header.
*/
public void setUpgrade(@Nullable String upgrade) {
setOrRemove(UPGRADE, upgrade);
}
/**
* Return the value of the {@code Upgrade} header.
*/
@Nullable
public String getUpgrade() {
return getFirst(UPGRADE);
}
/**
* Set the request header names (for example, "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 requestHeaders) {
set(VARY, toCommaDelimitedString(requestHeaders));
}
/**
* Return the request header names subject to content negotiation.
* @since 4.3
*/
public List getVary() {
return getValuesAsList(VARY);
}
/**
* Set the given date under the given header name after formatting it as a string
* using the RFC-1123 date-time formatter. The equivalent of
* {@link #set(String, String)} but for date headers.
* @since 5.0
*/
public void setZonedDateTime(String headerName, ZonedDateTime date) {
set(headerName, DATE_FORMATTER.format(date));
}
/**
* Set the given date under the given header name after formatting it as a string
* using the RFC-1123 date-time formatter. The equivalent of
* {@link #set(String, String)} but for date headers.
* @since 5.1.4
*/
public void setInstant(String headerName, Instant date) {
setZonedDateTime(headerName, ZonedDateTime.ofInstant(date, GMT));
}
/**
* Set the given date under the given header name after formatting it as a string
* using the RFC-1123 date-time formatter. The equivalent of
* {@link #set(String, String)} but for date headers.
* @since 3.2.4
* @see #setZonedDateTime(String, ZonedDateTime)
*/
public void setDate(String headerName, long date) {
setInstant(headerName, Instant.ofEpochMilli(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
* @see #getFirstZonedDateTime(String)
*/
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)
* @see #getFirstZonedDateTime(String, boolean)
*/
private long getFirstDate(String headerName, boolean rejectInvalid) {
ZonedDateTime zonedDateTime = getFirstZonedDateTime(headerName, rejectInvalid);
return (zonedDateTime != null ? zonedDateTime.toInstant().toEpochMilli() : -1);
}
/**
* Parse the first header value for the given header name as a date,
* return {@code null} 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 {@code null} if none
* @since 5.0
*/
@Nullable
public ZonedDateTime getFirstZonedDateTime(String headerName) {
return getFirstZonedDateTime(headerName, true);
}
/**
* Parse the first header value for the given header name as a date,
* return {@code null} 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 {@code null}
* in that case ({@code false})
* @return the parsed date header, or {@code null} if none (or invalid)
*/
@Nullable
private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInvalid) {
String headerValue = getFirst(headerName);
if (headerValue == null) {
// No header value sent at all
return null;
}
if (headerValue.length() >= 3) {
// Short "0" or "-1" like values are never valid HTTP date headers...
// Let's only bother with DateTimeFormatter parsing for long enough values.
// See https://stackoverflow.com/questions/12626699/if-modified-since-http-header-passed-by-ie9-includes-length
int parametersIndex = headerValue.indexOf(';');
if (parametersIndex != -1) {
headerValue = headerValue.substring(0, parametersIndex);
}
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
try {
return ZonedDateTime.parse(headerValue, dateFormatter);
}
catch (DateTimeParseException ex) {
// ignore
}
}
}
if (rejectInvalid) {
throw new IllegalArgumentException("Cannot parse date value \"" + headerValue +
"\" for \"" + headerName + "\" header");
}
return null;
}
/**
* Return all values of a given header name, even if this header is set
* multiple times.
* This method supports double-quoted values, as described in
* RFC
* 9110, section 5.5.
* @param headerName the header name
* @return all associated values
* @since 4.3
*/
public List getValuesAsList(String headerName) {
List values = get(headerName);
if (values != null) {
List result = new ArrayList<>();
for (String value : values) {
if (value != null) {
result.addAll(tokenizeQuoted(value));
}
}
return result;
}
return Collections.emptyList();
}
private static List tokenizeQuoted(String str) {
List tokens = new ArrayList<>();
boolean quoted = false;
boolean trim = true;
StringBuilder builder = new StringBuilder(str.length());
for (int i = 0; i < str.length(); ++i) {
char ch = str.charAt(i);
if (ch == '"') {
if (builder.isEmpty()) {
quoted = true;
}
else if (quoted) {
quoted = false;
trim = false;
}
else {
builder.append(ch);
}
}
else if (ch == '\\' && quoted && i < str.length() - 1) {
builder.append(str.charAt(++i));
}
else if (ch == ',' && !quoted) {
addToken(builder, tokens, trim);
builder.setLength(0);
trim = false;
}
else if (quoted || (!builder.isEmpty() && trim) || !Character.isWhitespace(ch)) {
builder.append(ch);
}
}
if (!builder.isEmpty()) {
addToken(builder, tokens, trim);
}
return tokens;
}
private static void addToken(StringBuilder builder, List tokens, boolean trim) {
String token = builder.toString();
if (trim) {
token = token.trim();
}
if (!token.isEmpty()) {
tokens.add(token);
}
}
/**
* Remove the well-known {@code "Content-*"} HTTP headers.
* Such headers should be cleared from the response if the intended
* body can't be written due to errors.
* @since 5.2.3
*/
public void clearContentHeaders() {
this.headers.remove(HttpHeaders.CONTENT_DISPOSITION);
this.headers.remove(HttpHeaders.CONTENT_ENCODING);
this.headers.remove(HttpHeaders.CONTENT_LANGUAGE);
this.headers.remove(HttpHeaders.CONTENT_LENGTH);
this.headers.remove(HttpHeaders.CONTENT_LOCATION);
this.headers.remove(HttpHeaders.CONTENT_RANGE);
this.headers.remove(HttpHeaders.CONTENT_TYPE);
}
/**
* Retrieve a combined result from the field values of the ETag header.
* @param name the header name
* @return the combined result
* @throws IllegalArgumentException if parsing fails
* @since 4.3
*/
protected List getETagValuesAsList(String name) {
List values = get(name);
if (values == null) {
return Collections.emptyList();
}
List result = new ArrayList<>();
for (String value : values) {
if (value != null) {
List tags = ETag.parse(value);
Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'");
for (ETag tag : tags) {
result.add(tag.formattedTag());
}
}
}
return result;
}
/**
* Retrieve a combined result from the field values of multivalued headers.
* @param headerName the header name
* @return the combined result
* @since 4.3
*/
@Nullable
protected String getFieldValues(String headerName) {
List 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 headerValues) {
StringJoiner joiner = new StringJoiner(", ");
for (String val : headerValues) {
if (val != null) {
joiner.add(val);
}
}
return joiner.toString();
}
/**
* Set the given header value, or remove the header if {@code null}.
* @param headerName the header name
* @param headerValue the header value, or {@code null} for none
*/
private void setOrRemove(String headerName, @Nullable String headerValue) {
if (headerValue != null) {
set(headerName, headerValue);
}
else {
remove(headerName);
}
}
// 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
@Nullable
public String getFirst(String headerName) {
return this.headers.getFirst(headerName);
}
/**
* 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, @Nullable String headerValue) {
this.headers.add(headerName, headerValue);
}
@Override
public void addAll(String key, List extends String> values) {
this.headers.addAll(key, values);
}
@Override
public void addAll(MultiValueMap values) {
this.headers.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, @Nullable String headerValue) {
this.headers.set(headerName, headerValue);
}
@Override
public void setAll(Map values) {
this.headers.setAll(values);
}
@Override
public Map toSingleValueMap() {
return this.headers.toSingleValueMap();
}
@Override
public Map asSingleValueMap() {
return this.headers.asSingleValueMap();
}
// Map implementation
@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
@Nullable
public List get(Object key) {
return this.headers.get(key);
}
@Override
public List put(String key, List value) {
return this.headers.put(key, value);
}
@Override
public List remove(Object key) {
return this.headers.remove(key);
}
@Override
public void putAll(Map extends String, ? extends List> map) {
this.headers.putAll(map);
}
@Override
public void clear() {
this.headers.clear();
}
@Override
public List putIfAbsent(String key, List value) {
return this.headers.putIfAbsent(key, value);
}
// Map/MultiValueMap methods that can have duplicate header names: size/keySet/values/entrySet/forEach
/**
* Return the number of headers in the collection. This can be inflated,
* see {@link HttpHeaders class level javadoc}.
*/
@Override
public int size() {
return this.headers.size();
}
/**
* Return a {@link Set} view of header names. This can include multiple
* casing variants of a given header name, see
* {@link HttpHeaders class level javadoc}.
*/
@Override
public Set keySet() {
return this.headers.keySet();
}
/**
* Return a {@link Collection} view of all the header values, reconstructed
* from iterating over the {@link #keySet()}. This can include duplicates if
* multiple casing variants of a given header name are tracked, see
* {@link HttpHeaders class level javadoc}.
*/
@Override
public Collection> values() {
return this.headers.values();
}
/**
* Return a {@link Set} views of header entries, reconstructed from
* iterating over the {@link #keySet()}. This can include duplicate entries
* if multiple casing variants of a given header name are tracked, see
* {@link HttpHeaders class level javadoc}.
* @see #headerSet()
*/
@Override
public Set>> entrySet() {
return this.headers.entrySet();
}
/**
* Perform an action over each header, as when iterated via
* {@link #entrySet()}. This can include duplicate entries
* if multiple casing variants of a given header name are tracked, see
* {@link HttpHeaders class level javadoc}.
* @param action the action to be performed for each entry
*/
@Override
public void forEach(BiConsumer super String, ? super List> action) {
this.headers.forEach(action);
}
/**
* Return a view of the headers as an entry {@code Set} of key-list pairs.
* Both {@link Iterator#remove()} and {@link java.util.Map.Entry#setValue}
* are supported and mutate the headers.
* This collection is guaranteed to contain one entry per header name
* even if the backing structure stores multiple casing variants of names,
* at the cost of first copying the names into a case-insensitive set for
* filtering the iteration.
* @return a {@code Set} view that iterates over all headers in a
* case-insensitive manner
* @since 6.1.15
*/
public Set>> headerSet() {
return new CaseInsensitiveEntrySet(this.headers);
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof HttpHeaders that && unwrap(this).equals(unwrap(that))));
}
@Override
public int hashCode() {
return this.headers.hashCode();
}
@Override
public String toString() {
return formatHeaders(this.headers);
}
/**
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
* Also caches the parsed representations of the "Accept" and "Content-Type" headers.
* @param headers the headers to expose
* @return a read-only variant of the headers, or the original headers as-is
* (in case it happens to be a read-only {@code HttpHeaders} instance already)
* @since 5.3
*/
public static HttpHeaders readOnlyHttpHeaders(MultiValueMap headers) {
return (headers instanceof HttpHeaders httpHeaders ? readOnlyHttpHeaders(httpHeaders) :
new ReadOnlyHttpHeaders(headers));
}
/**
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
* Also caches the parsed representations of the "Accept" and "Content-Type" headers.
* @param headers the headers to expose
* @return a read-only variant of the headers, or the original headers as-is if already read-only
*/
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
Assert.notNull(headers, "HttpHeaders must not be null");
return (headers instanceof ReadOnlyHttpHeaders ? headers : new ReadOnlyHttpHeaders(headers.headers));
}
/**
* Remove any read-only wrapper that may have been previously applied around
* the given headers via {@link #readOnlyHttpHeaders(HttpHeaders)}.
*
Once the writable instance is mutated, the read-only instance is likely
* to be out of sync and should be discarded.
* @param headers the headers to expose
* @return a writable variant of the headers, or the original headers as-is
* @since 5.1.1
* @deprecated as of 6.2 in favor of {@link #HttpHeaders(MultiValueMap)}.
*/
@Deprecated(since = "6.2", forRemoval = true)
public static HttpHeaders writableHttpHeaders(HttpHeaders headers) {
return new HttpHeaders(headers);
}
/**
* Helps to format HTTP header values, as HTTP header values themselves can
* contain comma-separated values, can become confusing with regular
* {@link Map} formatting that also uses commas between entries.
*
Additionally, this method displays the native list of header names
* with the mention {@code with native header names} if the underlying
* implementation stores multiple casing variants of header names (see
* {@link HttpHeaders class level javadoc}).
* @param headers the headers to format
* @return the headers to a String
* @since 5.1.4
*/
public static String formatHeaders(MultiValueMap headers) {
Set headerNames = toCaseInsensitiveSet(headers.keySet());
String suffix = "]";
if (headerNames.size() != headers.size()) {
suffix = "] with native header names " + headers.keySet();
}
return headerNames.stream()
.map(headerName -> {
List values = headers.get(headerName);
Assert.notNull(values, "Expected at least one value for header " + headerName);
return headerName + ":" + (values.size() == 1 ?
"\"" + values.get(0) + "\"" :
values.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")));
})
.collect(Collectors.joining(", ", "[", suffix));
}
/**
* Encode the given username and password into Basic Authentication credentials.
* The encoded credentials returned by this method can be supplied to
* {@link #setBasicAuth(String)} to set the Basic Authentication header.
* @param username the username
* @param password the password
* @param charset the charset to use to convert the credentials into an octet
* sequence. Defaults to {@linkplain StandardCharsets#ISO_8859_1 ISO-8859-1}.
* @throws IllegalArgumentException if {@code username} or {@code password}
* contains characters that cannot be encoded to the given charset
* @since 5.2
* @see #setBasicAuth(String)
* @see #setBasicAuth(String, String)
* @see #setBasicAuth(String, String, Charset)
* @see RFC 7617
*/
public static String encodeBasicAuth(String username, String password, @Nullable Charset charset) {
Assert.notNull(username, "Username must not be null");
Assert.doesNotContain(username, ":", "Username must not contain a colon");
Assert.notNull(password, "Password must not be null");
if (charset == null) {
charset = StandardCharsets.ISO_8859_1;
}
CharsetEncoder encoder = charset.newEncoder();
if (!encoder.canEncode(username) || !encoder.canEncode(password)) {
throw new IllegalArgumentException(
"Username or password contains characters that cannot be encoded to " + charset.displayName());
}
String credentialsString = username + ":" + password;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
return new String(encodedBytes, charset);
}
private static MultiValueMap unwrap(HttpHeaders headers) {
while (headers.headers instanceof HttpHeaders httpHeaders) {
headers = httpHeaders;
}
return headers.headers;
}
// Package-private: used in ResponseCookie
static String formatDate(long date) {
Instant instant = Instant.ofEpochMilli(date);
ZonedDateTime time = ZonedDateTime.ofInstant(instant, GMT);
return DATE_FORMATTER.format(time);
}
private static Set toCaseInsensitiveSet(Set originalSet) {
final Set deduplicatedSet = Collections.newSetFromMap(
new LinkedCaseInsensitiveMap<>(originalSet.size(), Locale.ROOT));
// add/addAll (put/putAll in LinkedCaseInsensitiveMap) retain the casing of the last occurrence.
// Here we prefer the first.
for (String header : originalSet) {
//noinspection RedundantCollectionOperation
if (!deduplicatedSet.contains(header)) {
deduplicatedSet.add(header);
}
}
return deduplicatedSet;
}
private static final class CaseInsensitiveEntrySet extends AbstractSet>> {
private final MultiValueMap headers;
private final Set deduplicatedNames;
public CaseInsensitiveEntrySet(MultiValueMap headers) {
this.headers = headers;
this.deduplicatedNames = toCaseInsensitiveSet(headers.keySet());
}
@Override
public Iterator>> iterator() {
return new CaseInsensitiveIterator(this.deduplicatedNames.iterator());
}
@Override
public int size() {
return this.deduplicatedNames.size();
}
private final class CaseInsensitiveIterator implements Iterator>> {
private final Iterator namesIterator;
@Nullable
private String currentName;
private CaseInsensitiveIterator(Iterator namesIterator) {
this.namesIterator = namesIterator;
this.currentName = null;
}
@Override
public boolean hasNext() {
return this.namesIterator.hasNext();
}
@Override
public Map.Entry> next() {
this.currentName = this.namesIterator.next();
return new CaseInsensitiveEntry(this.currentName);
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!CaseInsensitiveEntrySet.this.headers.containsKey(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
CaseInsensitiveEntrySet.this.headers.remove(this.currentName);
}
}
private final class CaseInsensitiveEntry implements Map.Entry> {
private final String key;
CaseInsensitiveEntry(String key) {
this.key = key;
}
@Override
public String getKey() {
return this.key;
}
@Override
public List getValue() {
return Objects.requireNonNull(CaseInsensitiveEntrySet.this.headers.get(this.key));
}
@Override
public List setValue(List value) {
List previousValues = Objects.requireNonNull(
CaseInsensitiveEntrySet.this.headers.get(this.key));
CaseInsensitiveEntrySet.this.headers.put(this.key, value);
return previousValues;
}
}
}
}