/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.integration.http.support; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.text.MessageFormat; 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.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.integration.mapping.HeaderMapper; import org.springframework.integration.support.utils.IntegrationUtils; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; import org.springframework.util.StringUtils; /** * Default {@link HeaderMapper} implementation for HTTP. * * @author Mark Fisher * @author Jeremy Grelle * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell * @author Artem Bilan * * @since 2.0 */ public class DefaultHttpHeaderMapper implements HeaderMapper<HttpHeaders>, BeanFactoryAware, InitializingBean { protected final Log logger = LogFactory.getLog(getClass()); public static final String ACCEPT = "Accept"; public static final String ACCEPT_CHARSET = "Accept-Charset"; public static final String ACCEPT_ENCODING = "Accept-Encoding"; public static final String ACCEPT_LANGUAGE = "Accept-Language"; public static final String ACCEPT_RANGES = "Accept-Ranges"; public static final String AGE = "Age"; public static final String ALLOW = "Allow"; public static final String AUTHORIZATION = "Authorization"; public static final String CACHE_CONTROL = "Cache-Control"; public static final String CONNECTION = "Connection"; public static final String CONTENT_ENCODING = "Content-Encoding"; public static final String CONTENT_LANGUAGE = "Content-Language"; public static final String CONTENT_LENGTH = "Content-Length"; public static final String CONTENT_LOCATION = "Content-Location"; public static final String CONTENT_MD5 = "Content-MD5"; public static final String CONTENT_RANGE = "Content-Range"; public static final String CONTENT_TYPE = "Content-Type"; public static final String CONTENT_DISPOSITION = "Content-Disposition"; public static final String COOKIE = "Cookie"; public static final String DATE = "Date"; public static final String ETAG = "ETag"; public static final String EXPECT = "Expect"; public static final String EXPIRES = "Expires"; public static final String FROM = "From"; public static final String HOST = "Host"; public static final String IF_MATCH = "If-Match"; public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; public static final String IF_NONE_MATCH = "If-None-Match"; public static final String IF_RANGE = "If-Range"; public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; public static final String LAST_MODIFIED = "Last-Modified"; public static final String LOCATION = "Location"; public static final String MAX_FORWARDS = "Max-Forwards"; public static final String PRAGMA = "Pragma"; public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; public static final String RANGE = "Range"; public static final String REFERER = "Referer"; public static final String REFRESH = "Refresh"; public static final String RETRY_AFTER = "Retry-After"; public static final String SERVER = "Server"; public static final String SET_COOKIE = "Set-Cookie"; public static final String TE = "TE"; public static final String TRAILER = "Trailer"; public static final String UPGRADE = "Upgrade"; public static final String USER_AGENT = "User-Agent"; public static final String VARY = "Vary"; public static final String VIA = "Via"; public static final String WARNING = "Warning"; public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; public static final String TRANSFER_ENCODING = "Transfer-Encoding"; private static final String[] HTTP_REQUEST_HEADER_NAMES = new String[] { ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, DATE, EXPECT, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, MAX_FORWARDS, PRAGMA, PROXY_AUTHORIZATION, RANGE, REFERER, TE, UPGRADE, USER_AGENT, VIA, WARNING }; private static final Set<String> HTTP_REQUEST_HEADER_NAMES_LOWER = new HashSet<>(); private static final String[] HTTP_RESPONSE_HEADER_NAMES = new String[] { ACCEPT_RANGES, AGE, ALLOW, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_MD5, CONTENT_RANGE, CONTENT_TYPE, CONTENT_DISPOSITION, TRANSFER_ENCODING, DATE, ETAG, EXPIRES, LAST_MODIFIED, LOCATION, PRAGMA, PROXY_AUTHENTICATE, REFRESH, RETRY_AFTER, SERVER, SET_COOKIE, TRAILER, VARY, VIA, WARNING, WWW_AUTHENTICATE }; private static final Set<String> HTTP_RESPONSE_HEADER_NAMES_LOWER = new HashSet<String>(); private static final String[] HTTP_REQUEST_HEADER_NAMES_OUTBOUND_EXCLUSIONS = new String[0]; private static final String[] HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS = new String[] { CONTENT_LENGTH, TRANSFER_ENCODING }; public static final String HTTP_REQUEST_HEADER_NAME_PATTERN = "HTTP_REQUEST_HEADERS"; public static final String HTTP_RESPONSE_HEADER_NAME_PATTERN = "HTTP_RESPONSE_HEADERS"; // Copy of 'org.springframework.http.HttpHeaders#DATE_FORMATS' protected static final DateTimeFormatter[] DATE_FORMATS = new DateTimeFormatter[] { DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US).withZone(ZoneId.of("GMT")), DateTimeFormatter.ofPattern("EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US).withZone(ZoneId.of("GMT")), DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(ZoneId.of("GMT")) }; static { for (String header : HTTP_REQUEST_HEADER_NAMES) { HTTP_REQUEST_HEADER_NAMES_LOWER.add(header.toLowerCase()); } for (String header : HTTP_RESPONSE_HEADER_NAMES) { HTTP_RESPONSE_HEADER_NAMES_LOWER.add(header.toLowerCase()); } } private volatile String[] outboundHeaderNames = new String[0]; private volatile String[] outboundHeaderNamesLowerWithContentType = new String[0]; private volatile String[] inboundHeaderNames = new String[0]; private volatile String[] inboundHeaderNamesLower = new String[0]; private volatile String[] excludedOutboundStandardRequestHeaderNames = new String[0]; private volatile String[] excludedInboundStandardResponseHeaderNames = new String[0]; private volatile String userDefinedHeaderPrefix = ""; private volatile boolean isDefaultOutboundMapper; private volatile boolean isDefaultInboundMapper; private volatile ConversionService conversionService; private volatile BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } protected BeanFactory getBeanFactory() { return this.beanFactory; } /** * Provide the header names that should be mapped to an HTTP request (for outbound adapters) * or HTTP response (for inbound adapters) from a Spring Integration Message's headers. * The values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched. * <p> Any non-standard headers will be prefixed with the value specified by * {@link DefaultHttpHeaderMapper#setUserDefinedHeaderPrefix(String)}. The default is 'X-'. * @param outboundHeaderNames The outbound header names. */ public void setOutboundHeaderNames(String[] outboundHeaderNames) { //NOSONAR - false positive if (HTTP_REQUEST_HEADER_NAMES == outboundHeaderNames) { this.isDefaultOutboundMapper = true; } else if (HTTP_RESPONSE_HEADER_NAMES == outboundHeaderNames) { this.isDefaultInboundMapper = true; } this.outboundHeaderNames = outboundHeaderNames != null ? Arrays.copyOf(outboundHeaderNames, outboundHeaderNames.length) : new String[0]; String[] outboundHeaderNamesLower = new String[this.outboundHeaderNames.length]; for (int i = 0; i < this.outboundHeaderNames.length; i++) { if (HTTP_REQUEST_HEADER_NAME_PATTERN.equals(this.outboundHeaderNames[i]) || HTTP_RESPONSE_HEADER_NAME_PATTERN.equals(this.outboundHeaderNames[i])) { outboundHeaderNamesLower[i] = this.outboundHeaderNames[i]; } else { outboundHeaderNamesLower[i] = this.outboundHeaderNames[i].toLowerCase(); } } this.outboundHeaderNamesLowerWithContentType = Arrays.copyOf(outboundHeaderNamesLower, this.outboundHeaderNames.length + 1); this.outboundHeaderNamesLowerWithContentType[this.outboundHeaderNamesLowerWithContentType.length - 1] = MessageHeaders.CONTENT_TYPE.toLowerCase(); } /** * Provide the header names that should be mapped from an HTTP request (for inbound * adapters) or HTTP response (for outbound adapters) to a Spring Integration * Message's headers. The values can also contain simple wildcard patterns (e.g. * "foo*" or "*foo") to be matched. * <p>This will match the header name directly or, for non-standard HTTP headers, it * will match the header name prefixed with the value specified by * {@link DefaultHttpHeaderMapper#setUserDefinedHeaderPrefix(String)}. The default for * that is an empty String. * @param inboundHeaderNames The inbound header names. */ public void setInboundHeaderNames(String[] inboundHeaderNames) { //NOSONAR - false positive this.inboundHeaderNames = inboundHeaderNames != null ? Arrays.copyOf(inboundHeaderNames, inboundHeaderNames.length) : new String[0]; this.inboundHeaderNamesLower = new String[this.inboundHeaderNames.length]; for (int i = 0; i < this.inboundHeaderNames.length; i++) { if (HTTP_REQUEST_HEADER_NAME_PATTERN.equals(this.inboundHeaderNames[i]) || HTTP_RESPONSE_HEADER_NAME_PATTERN.equals(this.inboundHeaderNames[i])) { this.inboundHeaderNamesLower[i] = this.inboundHeaderNames[i]; } else { this.inboundHeaderNamesLower[i] = this.inboundHeaderNames[i].toLowerCase(); } } } /** * Provide header names from the list of standard headers that should be suppressed when * mapping outbound endpoint request headers. * @param excludedOutboundStandardRequestHeaderNames the excludedStandardRequestHeaderNames to set */ public void setExcludedOutboundStandardRequestHeaderNames(String[] excludedOutboundStandardRequestHeaderNames) { Assert.notNull(excludedOutboundStandardRequestHeaderNames, "'excludedOutboundStandardRequestHeaderNames' must not be null"); this.excludedOutboundStandardRequestHeaderNames = Arrays.copyOf(excludedOutboundStandardRequestHeaderNames, excludedOutboundStandardRequestHeaderNames.length); } /** * Provide header names from the list of standard headers that should be suppressed when * mapping inbound endpoint response headers. * @param excludedInboundStandardResponseHeaderNames the excludedStandardResponseHeaderNames to set */ public void setExcludedInboundStandardResponseHeaderNames(String[] excludedInboundStandardResponseHeaderNames) { Assert.notNull(excludedInboundStandardResponseHeaderNames, "'excludedInboundStandardResponseHeaderNames' must not be null"); this.excludedInboundStandardResponseHeaderNames = Arrays.copyOf(excludedInboundStandardResponseHeaderNames, excludedInboundStandardResponseHeaderNames.length); } /** * Sets the prefix to use with user-defined (non-standard) headers. The default is an * empty string. * @param userDefinedHeaderPrefix The user defined header prefix. */ public void setUserDefinedHeaderPrefix(String userDefinedHeaderPrefix) { this.userDefinedHeaderPrefix = (userDefinedHeaderPrefix != null) ? userDefinedHeaderPrefix : ""; } /** * Map from the integration MessageHeaders to an HttpHeaders instance. * Depending on which type of adapter is using this mapper, the HttpHeaders might be * for an HTTP request (outbound adapter) or for an HTTP response (inbound adapter). */ @Override public void fromHeaders(MessageHeaders headers, HttpHeaders target) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("outboundHeaderNames={0}", CollectionUtils.arrayToList(this.outboundHeaderNames))); } for (Entry<String, Object> entry : headers.entrySet()) { String name = entry.getKey(); String lowerName = name.toLowerCase(); if (this.shouldMapOutboundHeader(lowerName)) { Object value = entry.getValue(); if (value != null) { if (!HTTP_REQUEST_HEADER_NAMES_LOWER.contains(lowerName) && !HTTP_RESPONSE_HEADER_NAMES_LOWER.contains(lowerName) && !MessageHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { // prefix the user-defined header names if not already prefixed name = StringUtils.startsWithIgnoreCase(name, this.userDefinedHeaderPrefix) ? name : this.userDefinedHeaderPrefix + name; } if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("setting headerName=[{0}], value={1}", name, value)); } this.setHttpHeader(target, name, value); } } } } /** * Map from an HttpHeaders instance to integration MessageHeaders. * Depending on which type of adapter is using this mapper, the HttpHeaders might be * from an HTTP request (inbound adapter) or from an HTTP response (outbound adapter). */ @Override public Map<String, Object> toHeaders(HttpHeaders source) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("inboundHeaderNames={0}", CollectionUtils.arrayToList(this.inboundHeaderNames))); } Map<String, Object> target = new HashMap<String, Object>(); Set<String> headerNames = source.keySet(); for (String name : headerNames) { String lowerName = name.toLowerCase(); if (this.shouldMapInboundHeader(lowerName)) { if (!HTTP_REQUEST_HEADER_NAMES_LOWER.contains(lowerName) && !HTTP_RESPONSE_HEADER_NAMES_LOWER.contains(lowerName)) { String prefixedName = StringUtils.startsWithIgnoreCase(name, this.userDefinedHeaderPrefix) ? name : this.userDefinedHeaderPrefix + name; Object value = source.containsKey(prefixedName) ? this.getHttpHeader(source, prefixedName) : this.getHttpHeader(source, name); if (value != null) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("setting headerName=[{0}], value={1}", name, value)); } this.setMessageHeader(target, name, value); } } else { Object value = this.getHttpHeader(source, name); if (value != null) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("setting headerName=[{0}], value={1}", name, value)); } if (CONTENT_TYPE.equalsIgnoreCase(name)) { name = MessageHeaders.CONTENT_TYPE; } this.setMessageHeader(target, name, value); } } } } return target; } @Override public void afterPropertiesSet() throws Exception { if (this.beanFactory != null) { this.conversionService = IntegrationUtils.getConversionService(this.beanFactory); } } protected final boolean containsElementIgnoreCase(String[] headerNames, String name) { for (String headerName : headerNames) { if (headerName.equalsIgnoreCase(name)) { return true; } } return false; } private boolean shouldMapOutboundHeader(String headerName) { String[] outboundHeaderNamesLower = this.outboundHeaderNamesLowerWithContentType; if (this.isDefaultInboundMapper) { /* * When using the default response header name list, suppress the * mapping of exclusions for specific headers. */ if (this.containsElementIgnoreCase(this.excludedInboundStandardResponseHeaderNames, headerName)) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped (excluded)", headerName)); } return false; } } else if (this.isDefaultOutboundMapper) { outboundHeaderNamesLower = this.outboundHeaderNamesLowerWithContentType; /* * When using the default request header name list, suppress the * mapping of exclusions for specific headers. */ if (this.containsElementIgnoreCase(this.excludedOutboundStandardRequestHeaderNames, headerName)) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped (excluded)", headerName)); } return false; } } return this.shouldMapHeader(headerName, outboundHeaderNamesLower); } protected final boolean shouldMapInboundHeader(String headerName) { return this.shouldMapHeader(headerName, this.inboundHeaderNamesLower); } /** * @param headerName the header name (lower cased). * @param patterns the patterns (lower cased). * @return true if should be mapped. */ private boolean shouldMapHeader(String headerName, String[] patterns) { if (patterns != null && patterns.length > 0) { for (String pattern : patterns) { if (PatternMatchUtils.simpleMatch(pattern, headerName)) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern)); } return true; } else if (HTTP_REQUEST_HEADER_NAME_PATTERN.equals(pattern) && HTTP_REQUEST_HEADER_NAMES_LOWER.contains(headerName)) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern)); } return true; } else if (HTTP_RESPONSE_HEADER_NAME_PATTERN.equals(pattern) && HTTP_RESPONSE_HEADER_NAMES_LOWER.contains(headerName)) { if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("headerName=[{0}] WILL be mapped, matched pattern={1}", headerName, pattern)); } return true; } } } if (this.logger.isDebugEnabled()) { this.logger.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped", headerName)); } return false; } private void setHttpHeader(HttpHeaders target, String name, Object value) { if (ACCEPT.equalsIgnoreCase(name)) { if (value instanceof Collection<?>) { Collection<?> values = (Collection<?>) value; if (!CollectionUtils.isEmpty(values)) { List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>(); for (Object type : values) { if (type instanceof MediaType) { acceptableMediaTypes.add((MediaType) type); } else if (type instanceof String) { acceptableMediaTypes.addAll(MediaType.parseMediaTypes((String) type)); } else { Class<?> clazz = (type != null) ? type.getClass() : null; throw new IllegalArgumentException( "Expected MediaType or String value for 'Accept' header value, but received: " + clazz); } } target.setAccept(acceptableMediaTypes); } } else if (value instanceof MediaType) { target.setAccept(Collections.singletonList((MediaType) value)); } else if (value instanceof String[]) { List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>(); for (String next : (String[]) value) { acceptableMediaTypes.add(MediaType.parseMediaType(next)); } target.setAccept(acceptableMediaTypes); } else if (value instanceof String) { target.setAccept(MediaType.parseMediaTypes((String) value)); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected MediaType or String value for 'Accept' header value, but received: " + clazz); } } else if (ACCEPT_CHARSET.equalsIgnoreCase(name)) { if (value instanceof Collection<?>) { Collection<?> values = (Collection<?>) value; if (!CollectionUtils.isEmpty(values)) { List<Charset> acceptableCharsets = new ArrayList<Charset>(); for (Object charset : values) { if (charset instanceof Charset) { acceptableCharsets.add((Charset) charset); } else if (charset instanceof String) { acceptableCharsets.add(Charset.forName((String) charset)); } else { Class<?> clazz = (charset != null) ? charset.getClass() : null; throw new IllegalArgumentException( "Expected Charset or String value for 'Accept-Charset' header value, but received: " + clazz); } } target.setAcceptCharset(acceptableCharsets); } } else if (value instanceof Charset[] || value instanceof String[]) { List<Charset> acceptableCharsets = new ArrayList<Charset>(); Object[] values = ObjectUtils.toObjectArray(value); for (Object charset : values) { if (charset instanceof Charset) { acceptableCharsets.add((Charset) charset); } else if (charset instanceof String) { acceptableCharsets.add(Charset.forName((String) charset)); } } target.setAcceptCharset(acceptableCharsets); } else if (value instanceof Charset) { target.setAcceptCharset(Collections.singletonList((Charset) value)); } else if (value instanceof String) { String[] charsets = StringUtils.commaDelimitedListToStringArray((String) value); List<Charset> acceptableCharsets = new ArrayList<Charset>(); for (String charset : charsets) { acceptableCharsets.add(Charset.forName(charset.trim())); } target.setAcceptCharset(acceptableCharsets); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Charset or String value for 'Accept-Charset' header value, but received: " + clazz); } } else if (ALLOW.equalsIgnoreCase(name)) { if (value instanceof Collection<?>) { Collection<?> values = (Collection<?>) value; if (!CollectionUtils.isEmpty(values)) { Set<HttpMethod> allowedMethods = new HashSet<HttpMethod>(); for (Object method : values) { if (method instanceof HttpMethod) { allowedMethods.add((HttpMethod) method); } else if (method instanceof String) { allowedMethods.add(HttpMethod.valueOf((String) method)); } else { Class<?> clazz = (method != null) ? method.getClass() : null; throw new IllegalArgumentException( "Expected HttpMethod or String value for 'Allow' header value, but received: " + clazz); } } target.setAllow(allowedMethods); } } else { if (value instanceof HttpMethod) { target.setAllow(Collections.singleton((HttpMethod) value)); } else if (value instanceof HttpMethod[]) { Set<HttpMethod> allowedMethods = new HashSet<HttpMethod>(); Collections.addAll(allowedMethods, (HttpMethod[]) value); target.setAllow(allowedMethods); } else if (value instanceof String || value instanceof String[]) { String[] values = (value instanceof String[]) ? (String[]) value : StringUtils.commaDelimitedListToStringArray((String) value); Set<HttpMethod> allowedMethods = new HashSet<HttpMethod>(); for (String next : values) { allowedMethods.add(HttpMethod.valueOf(next.trim())); } target.setAllow(allowedMethods); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected HttpMethod or String value for 'Allow' header value, but received: " + clazz); } } } else if (CACHE_CONTROL.equalsIgnoreCase(name)) { if (value instanceof String) { target.setCacheControl((String) value); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected String value for 'Cache-Control' header value, but received: " + clazz); } } else if (CONTENT_LENGTH.equalsIgnoreCase(name)) { if (value instanceof Number) { target.setContentLength(((Number) value).longValue()); } else if (value instanceof String) { target.setContentLength(Long.parseLong((String) value)); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Number or String value for 'Content-Length' header value, but received: " + clazz); } } else if (MessageHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { if (value instanceof MediaType) { target.setContentType((MediaType) value); } else if (value instanceof String) { target.setContentType(MediaType.parseMediaType((String) value)); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected MediaType or String value for 'Content-Type' header value, but received: " + clazz); } } else if (DATE.equalsIgnoreCase(name)) { if (value instanceof Date) { target.setDate(((Date) value).getTime()); } else if (value instanceof Number) { target.setDate(((Number) value).longValue()); } else if (value instanceof String) { try { target.setDate(Long.parseLong((String) value)); } catch (NumberFormatException e) { target.setDate(this.getFirstDate((String) value, DATE)); } } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Date, Number, or String value for 'Date' header value, but received: " + clazz); } } else if (ETAG.equalsIgnoreCase(name)) { if (value instanceof String) { target.setETag((String) value); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected String value for 'ETag' header value, but received: " + clazz); } } else if (EXPIRES.equalsIgnoreCase(name)) { if (value instanceof Date) { target.setExpires(((Date) value).getTime()); } else if (value instanceof Number) { target.setExpires(((Number) value).longValue()); } else if (value instanceof String) { try { target.setExpires(Long.parseLong((String) value)); } catch (NumberFormatException e) { target.setExpires(this.getFirstDate((String) value, EXPIRES)); } } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Date, Number, or String value for 'Expires' header value, but received: " + clazz); } } else if (IF_MODIFIED_SINCE.equalsIgnoreCase(name)) { if (value instanceof Date) { target.setIfModifiedSince(((Date) value).getTime()); } else if (value instanceof Number) { target.setIfModifiedSince(((Number) value).longValue()); } else if (value instanceof String) { try { target.setIfModifiedSince(Long.parseLong((String) value)); } catch (NumberFormatException e) { target.setIfModifiedSince(this.getFirstDate((String) value, IF_MODIFIED_SINCE)); } } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Date, Number, or String value for 'If-Modified-Since' header value, but received: " + clazz); } } else if (IF_UNMODIFIED_SINCE.equalsIgnoreCase(name)) { String ifUnmodifiedSinceValue = null; if (value instanceof Date) { ifUnmodifiedSinceValue = this.formatDate(((Date) value).getTime()); } else if (value instanceof Number) { ifUnmodifiedSinceValue = this.formatDate(((Number) value).longValue()); } else if (value instanceof String) { try { ifUnmodifiedSinceValue = this.formatDate(Long.parseLong((String) value)); } catch (NumberFormatException e) { long longValue = this.getFirstDate((String) value, IF_UNMODIFIED_SINCE); ifUnmodifiedSinceValue = this.formatDate(longValue); } } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Date, Number, or String value for 'If-Unmodified-Since' header value, but received: " + clazz); } target.set(IF_UNMODIFIED_SINCE, ifUnmodifiedSinceValue); } else if (IF_NONE_MATCH.equalsIgnoreCase(name)) { if (value instanceof String) { target.setIfNoneMatch((String) value); } else if (value instanceof String[]) { String delmitedString = StringUtils.arrayToCommaDelimitedString((String[]) value); target.setIfNoneMatch(delmitedString); } else if (value instanceof Collection) { Collection<?> values = (Collection<?>) value; if (!CollectionUtils.isEmpty(values)) { List<String> ifNoneMatchList = new ArrayList<String>(); for (Object next : values) { if (next instanceof String) { ifNoneMatchList.add((String) next); } else { Class<?> clazz = (next != null) ? next.getClass() : null; throw new IllegalArgumentException( "Expected String value for 'If-None-Match' header value, but received: " + clazz); } } target.setIfNoneMatch(ifNoneMatchList); } } } else if (LAST_MODIFIED.equalsIgnoreCase(name)) { if (value instanceof Date) { target.setLastModified(((Date) value).getTime()); } else if (value instanceof Number) { target.setLastModified(((Number) value).longValue()); } else if (value instanceof String) { try { target.setLastModified(Long.parseLong((String) value)); } catch (NumberFormatException e) { target.setLastModified(this.getFirstDate((String) value, LAST_MODIFIED)); } } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected Date, Number, or String value for 'Last-Modified' header value, but received: " + clazz); } } else if (LOCATION.equalsIgnoreCase(name)) { if (value instanceof URI) { target.setLocation((URI) value); } else if (value instanceof String) { try { target.setLocation(new URI((String) value)); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected URI or String value for 'Location' header value, but received: " + clazz); } } else if (PRAGMA.equalsIgnoreCase(name)) { if (value instanceof String) { target.setPragma((String) value); } else { Class<?> clazz = (value != null) ? value.getClass() : null; throw new IllegalArgumentException( "Expected String value for 'Pragma' header value, but received: " + clazz); } } else if (value instanceof String) { target.set(name, (String) value); } else if (value instanceof String[]) { for (String next : (String[]) value) { target.add(name, next); } } else if (value instanceof Iterable<?>) { for (Object next : (Iterable<?>) value) { String convertedValue = null; if (next instanceof String) { convertedValue = (String) next; } else { convertedValue = this.convertToString(value); } if (StringUtils.hasText(convertedValue)) { target.add(name, convertedValue); } else { this.logger.warn("Element of the header '" + name + "' with value '" + value + "' will not be set since it is not a String and no Converter is available. " + "Consider registering a Converter with ConversionService (e.g., <int:converter>)"); } } } else { String convertedValue = this.convertToString(value); if (StringUtils.hasText(convertedValue)) { target.set(name, convertedValue); } else { this.logger.warn("Header '" + name + "' with value '" + value + "' will not be set since it is not a String and no Converter is available. " + "Consider registering a Converter with ConversionService (e.g., <int:converter>)"); } } } protected Object getHttpHeader(HttpHeaders source, String name) { if (ACCEPT.equalsIgnoreCase(name)) { return source.getAccept(); } else if (ACCEPT_CHARSET.equalsIgnoreCase(name)) { return source.getAcceptCharset(); } else if (ALLOW.equalsIgnoreCase(name)) { return source.getAllow(); } else if (CACHE_CONTROL.equalsIgnoreCase(name)) { String cacheControl = source.getCacheControl(); return (StringUtils.hasText(cacheControl)) ? cacheControl : null; } else if (CONTENT_LENGTH.equalsIgnoreCase(name)) { long contentLength = source.getContentLength(); return (contentLength > -1) ? contentLength : null; } else if (CONTENT_TYPE.equalsIgnoreCase(name)) { return source.getContentType(); } else if (DATE.equalsIgnoreCase(name)) { long date = source.getDate(); return (date > -1) ? date : null; } else if (ETAG.equalsIgnoreCase(name)) { String eTag = source.getETag(); return (StringUtils.hasText(eTag)) ? eTag : null; } else if (EXPIRES.equalsIgnoreCase(name)) { try { long expires = source.getExpires(); return (expires > -1) ? expires : null; } catch (Exception e) { this.logger.debug(e.getMessage()); // According to RFC 2616 return null; } } else if (IF_NONE_MATCH.equalsIgnoreCase(name)) { return source.getIfNoneMatch(); } else if (IF_MODIFIED_SINCE.equalsIgnoreCase(name)) { long modifiedSince = source.getIfModifiedSince(); return (modifiedSince > -1) ? modifiedSince : null; } else if (IF_UNMODIFIED_SINCE.equalsIgnoreCase(name)) { String unmodifiedSince = source.getFirst(IF_UNMODIFIED_SINCE); return unmodifiedSince != null ? this.getFirstDate(unmodifiedSince, IF_UNMODIFIED_SINCE) : null; } else if (LAST_MODIFIED.equalsIgnoreCase(name)) { long lastModified = source.getLastModified(); return (lastModified > -1) ? lastModified : null; } else if (LOCATION.equalsIgnoreCase(name)) { return source.getLocation(); } else if (PRAGMA.equalsIgnoreCase(name)) { String pragma = source.getPragma(); return (StringUtils.hasText(pragma)) ? pragma : null; } return source.get(name); } private void setMessageHeader(Map<String, Object> target, String name, Object value) { if (ObjectUtils.isArray(value)) { Object[] values = ObjectUtils.toObjectArray(value); if (!ObjectUtils.isEmpty(values)) { if (values.length == 1) { target.put(name, values); } else { target.put(name, values[0]); } } } else if (value instanceof Collection<?>) { Collection<?> values = (Collection<?>) value; if (!CollectionUtils.isEmpty(values)) { if (values.size() == 1) { target.put(name, values.iterator().next()); } else { target.put(name, values); } } } else if (value != null) { target.put(name, value); } } protected String convertToString(Object value) { if (this.conversionService != null && this.conversionService.canConvert(TypeDescriptor.forObject(value), TypeDescriptor.valueOf(String.class))) { return this.conversionService.convert(value, String.class); } return null; } // Utility methods protected long getFirstDate(String headerValue, String headerName) { for (DateTimeFormatter dateFormat : DATE_FORMATS) { try { return dateFormat.parse(headerValue, ZonedDateTime::from) .toInstant() .toEpochMilli(); } catch (DateTimeParseException e) { // ignore } } throw new IllegalArgumentException("Cannot parse date value '" + headerValue + "' for '" + headerName + "' header"); } protected String formatDate(long date) { return DATE_FORMATS[0].format(Instant.ofEpochMilli(date)); } /** * Factory method for creating a basic outbound mapper instance. * This will map all standard HTTP request headers when sending an HTTP request, * and it will map all standard HTTP response headers when receiving an HTTP response. * @return The default outbound mapper. */ public static DefaultHttpHeaderMapper outboundMapper() { DefaultHttpHeaderMapper mapper = new DefaultHttpHeaderMapper(); setupDefaultOutboundMapper(mapper); return mapper; } /** * Subclasses can call this from a static outboundMapper() method to set up * standard header mappings for an outbound mapper. * @param mapper the mapper. */ protected static void setupDefaultOutboundMapper(DefaultHttpHeaderMapper mapper) { mapper.setOutboundHeaderNames(HTTP_REQUEST_HEADER_NAMES); mapper.setInboundHeaderNames(HTTP_RESPONSE_HEADER_NAMES); mapper.setExcludedOutboundStandardRequestHeaderNames(HTTP_REQUEST_HEADER_NAMES_OUTBOUND_EXCLUSIONS); } /** * Factory method for creating a basic inbound mapper instance. * This will map all standard HTTP request headers when receiving an HTTP request, * and it will map all standard HTTP response headers when sending an HTTP response. * @return The default inbound mapper. */ public static DefaultHttpHeaderMapper inboundMapper() { DefaultHttpHeaderMapper mapper = new DefaultHttpHeaderMapper(); setupDefaultInboundMapper(mapper); return mapper; } /** * Subclasses can call this from a static inboundMapper() method to set up * standard header mappings for an inbound mapper. * @param mapper the mapper. */ protected static void setupDefaultInboundMapper(DefaultHttpHeaderMapper mapper) { mapper.setInboundHeaderNames(HTTP_REQUEST_HEADER_NAMES); mapper.setOutboundHeaderNames(HTTP_RESPONSE_HEADER_NAMES); mapper.setExcludedInboundStandardResponseHeaderNames(HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS); } }