All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.http.HttpHeaders Maven / Gradle / Ivy

There is a newer version: 6.2.0
Show newest version
/*
 * 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.LinkedHashSet;
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"); 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); return (StringUtils.hasText(value) ? Locale.LanguageRange.parse(value) : 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 = new LinkedHashSet<>(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. */ 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. *

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 etag) { if (etag != null) { Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/\""), "ETag does not start with W/\" or \""); Assert.isTrue(etag.endsWith("\""), "ETag does not end with \""); set(ETAG, etag); } 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, null); } } /** * 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 (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 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 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(); } // 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> 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> 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 */ 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)}. * @param headers the headers to expose * @return a writable variant of the headers, or the original headers as-is * @since 5.1.1 */ public static HttpHeaders writableHttpHeaders(HttpHeaders headers) { Assert.notNull(headers, "HttpHeaders must not be null"); if (headers == EMPTY) { return new HttpHeaders(); } while (headers.headers instanceof HttpHeaders wrapped) { headers = wrapped; } return new HttpHeaders(headers.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); 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; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy