/*
 * Copyright 2002-2019 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.integration.http.support;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.text.MessageFormat;
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.lang.Nullable;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
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 {

	private static final String UNUSED = "unused";

	protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR - final

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ACCEPT}
	 */
	@Deprecated
	public static final String ACCEPT = HttpHeaders.ACCEPT;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ACCEPT_CHARSET}
	 */
	@Deprecated
	public static final String ACCEPT_CHARSET = HttpHeaders.ACCEPT_CHARSET;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ACCEPT_ENCODING}
	 */
	@Deprecated
	public static final String ACCEPT_ENCODING = HttpHeaders.ACCEPT_ENCODING;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ACCEPT_LANGUAGE}
	 */
	@Deprecated
	public static final String ACCEPT_LANGUAGE = HttpHeaders.ACCEPT_LANGUAGE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ACCEPT_RANGES}
	 */
	@Deprecated
	public static final String ACCEPT_RANGES = HttpHeaders.ACCEPT_RANGES;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#AGE}
	 */
	@Deprecated
	public static final String AGE = HttpHeaders.AGE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ALLOW}
	 */
	@Deprecated
	public static final String ALLOW = HttpHeaders.ALLOW;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#AUTHORIZATION}
	 */
	@Deprecated
	public static final String AUTHORIZATION = HttpHeaders.AUTHORIZATION;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CACHE_CONTROL}
	 */
	@Deprecated
	public static final String CACHE_CONTROL = HttpHeaders.CACHE_CONTROL;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONNECTION}
	 */
	@Deprecated
	public static final String CONNECTION = HttpHeaders.CONNECTION;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_ENCODING}
	 */
	@Deprecated
	public static final String CONTENT_ENCODING = HttpHeaders.CONTENT_ENCODING;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_LANGUAGE}
	 */
	@Deprecated
	public static final String CONTENT_LANGUAGE = HttpHeaders.CONTENT_LANGUAGE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_LENGTH}
	 */
	@Deprecated
	public static final String CONTENT_LENGTH = HttpHeaders.CONTENT_LENGTH;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_LOCATION}
	 */
	@Deprecated
	public static final String CONTENT_LOCATION = HttpHeaders.CONTENT_LOCATION;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_RANGE}
	 */
	@Deprecated
	public static final String CONTENT_RANGE = HttpHeaders.CONTENT_RANGE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_TYPE}
	 */
	@Deprecated
	public static final String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#CONTENT_DISPOSITION}
	 */
	@Deprecated
	public static final String CONTENT_DISPOSITION = HttpHeaders.CONTENT_DISPOSITION;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#COOKIE}
	 */
	@Deprecated
	public static final String COOKIE = HttpHeaders.COOKIE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#DATE}
	 */
	@Deprecated
	public static final String DATE = HttpHeaders.DATE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#ETAG}
	 */
	@Deprecated
	public static final String ETAG = HttpHeaders.ETAG;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#EXPECT}
	 */
	@Deprecated
	public static final String EXPECT = HttpHeaders.EXPECT;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#EXPIRES}
	 */
	@Deprecated
	public static final String EXPIRES = HttpHeaders.EXPIRES;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#FROM}
	 */
	@Deprecated
	public static final String FROM = HttpHeaders.FROM;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#HOST}
	 */
	@Deprecated
	public static final String HOST = HttpHeaders.HOST;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#IF_MATCH}
	 */
	@Deprecated
	public static final String IF_MATCH = HttpHeaders.IF_MATCH;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#IF_MODIFIED_SINCE}
	 */
	@Deprecated
	public static final String IF_MODIFIED_SINCE = HttpHeaders.IF_MODIFIED_SINCE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#IF_NONE_MATCH}
	 */
	@Deprecated
	public static final String IF_NONE_MATCH = HttpHeaders.IF_NONE_MATCH;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#IF_RANGE}
	 */
	@Deprecated
	public static final String IF_RANGE = HttpHeaders.IF_RANGE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#IF_UNMODIFIED_SINCE}
	 */
	@Deprecated
	public static final String IF_UNMODIFIED_SINCE = HttpHeaders.IF_UNMODIFIED_SINCE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#LAST_MODIFIED}
	 */
	@Deprecated
	public static final String LAST_MODIFIED = HttpHeaders.LAST_MODIFIED;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#LOCATION}
	 */
	@Deprecated
	public static final String LOCATION = HttpHeaders.LOCATION;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#MAX_FORWARDS}
	 */
	@Deprecated
	public static final String MAX_FORWARDS = HttpHeaders.MAX_FORWARDS;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#PRAGMA}
	 */
	@Deprecated
	public static final String PRAGMA = HttpHeaders.PRAGMA;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#PROXY_AUTHENTICATE}
	 */
	@Deprecated
	public static final String PROXY_AUTHENTICATE = HttpHeaders.PROXY_AUTHENTICATE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#PROXY_AUTHORIZATION}
	 */
	@Deprecated
	public static final String PROXY_AUTHORIZATION = HttpHeaders.PROXY_AUTHORIZATION;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#RANGE}
	 */
	@Deprecated
	public static final String RANGE = HttpHeaders.RANGE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#REFERER}
	 */
	@Deprecated
	public static final String REFERER = HttpHeaders.REFERER;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#RETRY_AFTER}
	 */
	@Deprecated
	public static final String RETRY_AFTER = HttpHeaders.RETRY_AFTER;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#SERVER}
	 */
	@Deprecated
	public static final String SERVER = HttpHeaders.SERVER;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#SET_COOKIE}
	 */
	@Deprecated
	public static final String SET_COOKIE = HttpHeaders.SET_COOKIE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#TE}
	 */
	@Deprecated
	public static final String TE = HttpHeaders.TE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#TRAILER}
	 */
	@Deprecated
	public static final String TRAILER = HttpHeaders.TRAILER;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#UPGRADE}
	 */
	@Deprecated
	public static final String UPGRADE = HttpHeaders.UPGRADE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#USER_AGENT}
	 */
	@Deprecated
	public static final String USER_AGENT = HttpHeaders.USER_AGENT;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#VARY}
	 */
	@Deprecated
	public static final String VARY = HttpHeaders.VARY;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#VIA}
	 */
	@Deprecated
	public static final String VIA = HttpHeaders.VIA;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#WARNING}
	 */
	@Deprecated
	public static final String WARNING = HttpHeaders.WARNING;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#WWW_AUTHENTICATE}
	 */
	@Deprecated
	public static final String WWW_AUTHENTICATE = HttpHeaders.WWW_AUTHENTICATE;

	/**
	 * @deprecated since 5.2 in favor of {@link HttpHeaders#TRANSFER_ENCODING}
	 */
	@Deprecated
	public static final String TRANSFER_ENCODING = HttpHeaders.TRANSFER_ENCODING;

	public static final String CONTENT_MD5 = "Content-MD5";

	public static final String REFRESH = "Refresh";

	private static final String ACCEPT_LOWER = "accept";

	private static final String ACCEPT_CHARSET_LOWER = "accept-charset";

	private static final String ALLOW_LOWER = "allow";

	private static final String CACHE_CONTROL_LOWER = "cache-control";

	private static final String CONTENT_LENGTH_LOWER = "content-length";

	private static final String CONTENT_TYPE_LOWER = "content-type";

	private static final String DATE_LOWER = "date";

	private static final String ETAG_LOWER = "etag";

	private static final String EXPIRES_LOWER = "expires";

	private static final String IF_MODIFIED_SINCE_LOWER = "if-modified-since";

	private static final String IF_NONE_MATCH_LOWER = "if-none-match";

	private static final String IF_UNMODIFIED_SINCE_LOWER = "if-unmodified-since";

	private static final String LAST_MODIFIED_LOWER = "last-modified";

	private static final String LOCATION_LOWER = "location";

	private static final String PRAGMA_LOWER = "pragma";

	private static final String[] HTTP_REQUEST_HEADER_NAMES =
			{
					HttpHeaders.ACCEPT,
					HttpHeaders.ACCEPT_CHARSET,
					HttpHeaders.ACCEPT_ENCODING,
					HttpHeaders.ACCEPT_LANGUAGE,
					HttpHeaders.ACCEPT_RANGES,
					HttpHeaders.AUTHORIZATION,
					HttpHeaders.CACHE_CONTROL,
					HttpHeaders.CONNECTION,
					HttpHeaders.CONTENT_LENGTH,
					HttpHeaders.CONTENT_TYPE,
					HttpHeaders.COOKIE,
					HttpHeaders.DATE,
					HttpHeaders.EXPECT,
					HttpHeaders.FROM,
					HttpHeaders.HOST,
					HttpHeaders.IF_MATCH,
					HttpHeaders.IF_MODIFIED_SINCE,
					HttpHeaders.IF_NONE_MATCH,
					HttpHeaders.IF_RANGE,
					HttpHeaders.IF_UNMODIFIED_SINCE,
					HttpHeaders.MAX_FORWARDS,
					HttpHeaders.PRAGMA,
					HttpHeaders.PROXY_AUTHORIZATION,
					HttpHeaders.RANGE,
					HttpHeaders.REFERER,
					HttpHeaders.TE,
					HttpHeaders.UPGRADE,
					HttpHeaders.USER_AGENT,
					HttpHeaders.VIA,
					HttpHeaders.WARNING
			};

	private static final Set<String> HTTP_REQUEST_HEADER_NAMES_LOWER = new HashSet<>();

	private static final String[] HTTP_RESPONSE_HEADER_NAMES =
			{
					HttpHeaders.ACCEPT_RANGES,
					HttpHeaders.AGE,
					HttpHeaders.ALLOW,
					HttpHeaders.CACHE_CONTROL,
					HttpHeaders.CONNECTION,
					HttpHeaders.CONTENT_ENCODING,
					HttpHeaders.CONTENT_LANGUAGE,
					HttpHeaders.CONTENT_LENGTH,
					HttpHeaders.CONTENT_LOCATION,
					CONTENT_MD5,
					HttpHeaders.CONTENT_RANGE,
					HttpHeaders.CONTENT_TYPE,
					HttpHeaders.CONTENT_DISPOSITION,
					HttpHeaders.TRANSFER_ENCODING,
					HttpHeaders.DATE,
					HttpHeaders.ETAG,
					HttpHeaders.EXPIRES,
					HttpHeaders.LAST_MODIFIED,
					HttpHeaders.LOCATION,
					HttpHeaders.PRAGMA,
					HttpHeaders.PROXY_AUTHENTICATE,
					REFRESH,
					HttpHeaders.RETRY_AFTER,
					HttpHeaders.SERVER,
					HttpHeaders.SET_COOKIE,
					HttpHeaders.TRAILER,
					HttpHeaders.VARY,
					HttpHeaders.VIA,
					HttpHeaders.WARNING,
					HttpHeaders.WWW_AUTHENTICATE
			};

	private static final Set<String> HTTP_RESPONSE_HEADER_NAMES_LOWER = new HashSet<>();

	private static final String[] HTTP_REQUEST_HEADER_NAMES_OUTBOUND_EXCLUSIONS = { };

	private static final String[] HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS =
			{ HttpHeaders.CONTENT_LENGTH, HttpHeaders.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 = {
			DateTimeFormatter.RFC_1123_DATE_TIME,
			DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss zz", Locale.US),
			DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(ZoneId.of("GMT")) // NOSONAR
	};

	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 = {};

	private volatile String[] outboundHeaderNamesLowerWithContentType = {};

	private volatile String[] inboundHeaderNames = {};

	private volatile String[] inboundHeaderNamesLower = {};

	private volatile String[] excludedOutboundStandardRequestHeaderNames = {};

	private volatile String[] excludedInboundStandardResponseHeaderNames = {};

	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) {
		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 inboundHeaderNamesArg The inbound header names.
	 */
	public void setInboundHeaderNames(String... inboundHeaderNamesArg) {
		this.inboundHeaderNames =
				inboundHeaderNamesArg != null
						? Arrays.copyOf(inboundHeaderNamesArg, inboundHeaderNamesArg.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");
		Assert.noNullElements(excludedOutboundStandardRequestHeaderNames,
				"'excludedOutboundStandardRequestHeaderNames' must not have null elements");
		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");
		Assert.noNullElements(excludedInboundStandardResponseHeaderNames,
				"'excludedInboundStandardResponseHeaderNames' must not have null elements");
		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 : "";
	}

	@Override
	public void afterPropertiesSet() {
		if (this.beanFactory != null) {
			this.conversionService = IntegrationUtils.getConversionService(this.beanFactory);
		}
	}

	/**
	 * 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("outboundHeaderNames=" + Arrays.toString(this.outboundHeaderNames));
		}
		for (Entry<String, Object> entry : headers.entrySet()) {
			String name = entry.getKey();
			Object value = entry.getValue();
			String lowerName = name.toLowerCase();
			if (value != null && shouldMapOutboundHeader(lowerName)) {
				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));
				}
				setHttpHeader(target, name, value);
			}
		}
	}

	private void setHttpHeader(HttpHeaders target, String name, Object value) { // NOSONAR
		switch (name.toLowerCase()) {
			case ACCEPT_LOWER:
				setAccept(target, value);
				break;
			case ACCEPT_CHARSET_LOWER:
				setAcceptCharset(target, value);
				break;
			case ALLOW_LOWER:
				setAllow(target, value);
				break;
			case CACHE_CONTROL_LOWER:
				setCacheControl(target, value);
				break;
			case CONTENT_LENGTH_LOWER:
				setContentLength(target, value);
				break;
			case "contenttype": // Lower case for MessageHeaders.CONTENT_TYPE
				setContentType(target, value);
				break;
			case DATE_LOWER:
				setDate(target, value);
				break;
			case ETAG_LOWER:
				setETag(target, value);
				break;
			case EXPIRES_LOWER:
				setExpires(target, value);
				break;
			case IF_MODIFIED_SINCE_LOWER:
				setIfModifiedSince(target, value);
				break;
			case IF_UNMODIFIED_SINCE_LOWER:
				setIfUnmodifiedSince(target, value);
				break;
			case IF_NONE_MATCH_LOWER:
				setIfNoneMatch(target, value);
				break;
			case LAST_MODIFIED_LOWER:
				setLastModified(target, value);
				break;
			case LOCATION_LOWER:
				setLocation(target, value);
				break;
			case PRAGMA_LOWER:
				setPragma(target, value);
				break;
			default:
				if (value instanceof String) {
					target.set(name, (String) value);
				}
				else if (value instanceof String[]) {
					target.addAll(name, Arrays.asList((String[]) value));
				}
				else if (value instanceof Iterable<?>) {
					setIterableHeader(target, name, value);
				}
				else {
					setPlainHeader(target, name, value);
				}
		}
	}

	private void setAccept(HttpHeaders target, Object value) {
		Collection<?> valuesToAccept = valueToCollection(value);
		if (!CollectionUtils.isEmpty(valuesToAccept)) {
			List<MediaType> acceptableMediaTypes = new ArrayList<>();
			for (Object type : valuesToAccept) {
				if (type instanceof MimeType) {
					acceptableMediaTypes.add(MediaType.asMediaType((MimeType) type));
				}
				else if (type instanceof String) {
					acceptableMediaTypes.addAll(MediaType.parseMediaTypes((String) type));
				}
				else {
					throwIllegalArgumentForUnexpectedValue(
							"Expected org.springframework.util.MimeType " +
									"or String value for 'Accept' header value, but received: ", type);

				}
			}
			target.setAccept(acceptableMediaTypes);
		}
	}

	private void setAcceptCharset(HttpHeaders target, Object value) {
		Collection<?> valuesToConvert = valueToCollection(value);
		if (!CollectionUtils.isEmpty(valuesToConvert)) {
			List<Charset> acceptableCharsets = new ArrayList<>();
			for (Object charset : valuesToConvert) {
				if (charset instanceof Charset) {
					acceptableCharsets.add((Charset) charset);
				}
				else if (charset instanceof String) {
					String[] charsets = StringUtils.delimitedListToStringArray((String) charset, ",", " ");
					for (String charset2 : charsets) {
						acceptableCharsets.add(Charset.forName(charset2));
					}
				}
				else {
					throwIllegalArgumentForUnexpectedValue(
							"Expected Charset or String value for 'Accept-Charset' header value, but received: ",
							charset);
				}
			}
			target.setAcceptCharset(acceptableCharsets);
		}
	}

	private void setAllow(HttpHeaders target, Object value) {
		Collection<?> valuesToConvert = valueToCollection(value);
		if (!CollectionUtils.isEmpty(valuesToConvert)) {
			Set<HttpMethod> allowedMethods = new HashSet<>();
			for (Object method : valuesToConvert) {
				if (method instanceof HttpMethod) {
					allowedMethods.add((HttpMethod) method);
				}
				else if (method instanceof String) {
					String[] methods = StringUtils.delimitedListToStringArray((String) method, ",", " ");
					for (String method2 : methods) {
						allowedMethods.add(HttpMethod.valueOf(method2));
					}
				}
				else {
					throwIllegalArgumentForUnexpectedValue(
							"Expected HttpMethod or String value for 'Allow' header value, but received: ", method);
				}
			}
			target.setAllow(allowedMethods);
		}
	}

	private Collection<?> valueToCollection(Object value) {
		Collection<?> valuesToConvert;
		if (value instanceof Collection<?>) {
			valuesToConvert = (Collection<?>) value;
		}
		else if (value.getClass().isArray()) {
			valuesToConvert = Arrays.asList(ObjectUtils.toObjectArray(value));
		}
		else {
			valuesToConvert = Collections.singleton(value);
		}
		return valuesToConvert;
	}

	private void setCacheControl(HttpHeaders target, Object value) {
		if (value instanceof String) {
			target.setCacheControl((String) value);
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected String value for 'Cache-Control' header value, but received: ", value);
		}
	}

	private void setContentLength(HttpHeaders target, Object value) {
		if (value instanceof Number) {
			target.setContentLength(((Number) value).longValue());
		}
		else if (value instanceof String) {
			target.setContentLength(Long.parseLong((String) value));
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected Number or String value for 'Content-Length' header value, but received: ", value);
		}
	}

	private void setContentType(HttpHeaders target, Object value) {
		if (value instanceof MimeType) {
			target.setContentType(MediaType.asMediaType((MimeType) value));
		}
		else if (value instanceof String) {
			target.setContentType(MediaType.parseMediaType((String) value));
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected org.springframework.util.MimeType " +
							"or String value for 'Content-Type' header value, but received: ", value);
		}
	}

	private void setDate(HttpHeaders target, Object value) {
		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 (@SuppressWarnings(UNUSED) NumberFormatException e) {
				target.setDate(getFirstDate((String) value, HttpHeaders.DATE));
			}
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected Date, Number, or String value for 'Date' header value, but received: ", value);
		}
	}

	private void setETag(HttpHeaders target, Object value) {
		if (value instanceof String) {
			target.setETag((String) value);
		}
		else {
			throwIllegalArgumentForUnexpectedValue("Expected String value for 'ETag' header value, but received: ",
					value);
		}
	}

	private void setExpires(HttpHeaders target, Object value) {
		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 (@SuppressWarnings(UNUSED) NumberFormatException e) {
				target.setExpires(getFirstDate((String) value, HttpHeaders.EXPIRES));
			}
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected Date, Number, or String value for 'Expires' header value, but received: ", value);
		}
	}

	private void setIfModifiedSince(HttpHeaders target, Object value) {
		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 (@SuppressWarnings(UNUSED) NumberFormatException e) {
				target.setIfModifiedSince(getFirstDate((String) value, HttpHeaders.IF_MODIFIED_SINCE));
			}
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected Date, Number, or String value for 'If-Modified-Since' header value, but received: ",
					value);
		}
	}

	private void setIfUnmodifiedSince(HttpHeaders target, Object value) {
		if (value instanceof Date) {
			target.setIfUnmodifiedSince(((Date) value).getTime());
		}
		else if (value instanceof Number) {
			target.setIfUnmodifiedSince(((Number) value).longValue());
		}
		else if (value instanceof String) {
			try {
				target.setIfUnmodifiedSince(Long.parseLong((String) value));
			}
			catch (@SuppressWarnings(UNUSED) NumberFormatException e) {
				target.setIfUnmodifiedSince(getFirstDate((String) value, HttpHeaders.IF_UNMODIFIED_SINCE));
			}
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected Date, Number, or String value for 'If-Unmodified-Since' header value, but received: ",
					value);
		}
	}

	private void setIfNoneMatch(HttpHeaders target, Object value) {
		Collection<?> valuesToAccept = valueToCollection(value);
		List<String> ifNoneMatchList = new ArrayList<>();

		for (Object match : valuesToAccept) {
			if (match instanceof String) {
				ifNoneMatchList.add((String) match);
			}
			else {
				throwIllegalArgumentForUnexpectedValue(
						"Expected String value for 'If-None-Match' header value, but received: ", value);
			}
		}

		if (!ifNoneMatchList.isEmpty()) {
			target.setIfNoneMatch(ifNoneMatchList);
		}
	}

	private void setLastModified(HttpHeaders target, Object value) {
		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 (@SuppressWarnings(UNUSED) NumberFormatException e) {
				target.setLastModified(getFirstDate((String) value, HttpHeaders.LAST_MODIFIED));
			}
		}
		else {
			throwIllegalArgumentForUnexpectedValue(
					"Expected Date, Number, or String value for 'Last-Modified' header value, but received: ", value);
		}
	}

	private void setLocation(HttpHeaders target, Object value) {
		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 {
			throwIllegalArgumentForUnexpectedValue(
					"Expected URI or String value for 'Location' header value, but received: ", value);
		}
	}

	private void setPragma(HttpHeaders target, Object value) {
		if (value instanceof String) {
			target.setPragma((String) value);
		}
		else {
			throwIllegalArgumentForUnexpectedValue("Expected String value for 'Pragma' header value, but received: ",
					value);
		}
	}

	private void throwIllegalArgumentForUnexpectedValue(String message, @Nullable Object value) {
		throw new IllegalArgumentException(message + (value != null ? value.getClass() : null));
	}

	private void setIterableHeader(HttpHeaders target, String name, Object value) {
		for (Object next : (Iterable<?>) value) {
			String convertedValue;
			if (next instanceof String) {
				convertedValue = (String) next;
			}
			else {
				convertedValue = 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>)");
			}
		}
	}

	private void setPlainHeader(HttpHeaders target, String name, Object value) {
		String convertedValue = 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>)");
		}
	}

	/**
	 * 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("inboundHeaderNames=" + Arrays.toString(this.inboundHeaderNames));
		}
		Map<String, Object> target = new HashMap<>();
		Set<String> headerNames = source.keySet();
		for (String name : headerNames) {
			String lowerName = name.toLowerCase();
			if (shouldMapInboundHeader(lowerName)) {
				if (!HTTP_REQUEST_HEADER_NAMES_LOWER.contains(lowerName)
						&& !HTTP_RESPONSE_HEADER_NAMES_LOWER.contains(lowerName)) {
					populateUserDefinedHeader(source, target, name);
				}
				else {
					populateStandardHeader(source, target, name);
				}
			}
		}
		return target;
	}

	private void populateUserDefinedHeader(HttpHeaders source, Map<String, Object> target, String name) {
		String prefixedName = StringUtils.startsWithIgnoreCase(name, this.userDefinedHeaderPrefix)
				? name
				: this.userDefinedHeaderPrefix + name;
		Object value = source.containsKey(prefixedName)
				? getHttpHeader(source, prefixedName)
				: getHttpHeader(source, name);
		if (value != null) {
			setMessageHeader(target, name, value);
		}
	}

	private void populateStandardHeader(HttpHeaders source, Map<String, Object> target, String name) {
		Object value = getHttpHeader(source, name);
		if (value != null) {
			setMessageHeader(target,
					HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) ? MessageHeaders.CONTENT_TYPE : name, value);
		}
	}

	protected Object getHttpHeader(HttpHeaders source, String name) { // NOSONAR
		switch (name.toLowerCase()) {
			case ACCEPT_LOWER:
				return source.getAccept();
			case ACCEPT_CHARSET_LOWER:
				return source.getAcceptCharset();
			case ALLOW_LOWER:
				return source.getAllow();
			case CACHE_CONTROL_LOWER:
				String cacheControl = source.getCacheControl();
				return (StringUtils.hasText(cacheControl)) ? cacheControl : null;
			case CONTENT_LENGTH_LOWER:
				long contentLength = source.getContentLength();
				return (contentLength > -1) ? contentLength : null;
			case CONTENT_TYPE_LOWER:
				return source.getContentType();
			case DATE_LOWER:
				long date = source.getDate();
				return (date > -1) ? date : null;
			case ETAG_LOWER:
				String eTag = source.getETag();
				return (StringUtils.hasText(eTag)) ? eTag : null;
			case EXPIRES_LOWER:
				long expires = source.getExpires();
				return (expires > -1) ? expires : null;
			case IF_NONE_MATCH_LOWER:
				return source.getIfNoneMatch();
			case IF_MODIFIED_SINCE_LOWER:
				long modifiedSince = source.getIfModifiedSince();
				return (modifiedSince > -1) ? modifiedSince : null;
			case IF_UNMODIFIED_SINCE_LOWER:
				long unmodifiedSince = source.getIfUnmodifiedSince();
				return (unmodifiedSince > -1) ? unmodifiedSince : null;
			case LAST_MODIFIED_LOWER:
				long lastModified = source.getLastModified();
				return (lastModified > -1) ? lastModified : null;
			case LOCATION_LOWER:
				return source.getLocation();
			case PRAGMA_LOWER:
				String pragma = source.getPragma();
				return (StringUtils.hasText(pragma)) ? pragma : null;
			default:
				return source.get(name);
		}
	}

	private void setMessageHeader(Map<String, Object> target, String name, Object value) {
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(MessageFormat.format("setting headerName=[{0}], value={1}", name, 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);
		}
	}

	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 (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 (containsElementIgnoreCase(this.excludedOutboundStandardRequestHeaderNames, headerName)) {
				if (this.logger.isDebugEnabled()) {
					this.logger
							.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped (excluded)", headerName));
				}
				return false;
			}
		}
		return shouldMapHeader(headerName, outboundHeaderNamesLower);
	}

	protected final boolean shouldMapInboundHeader(String headerName) {
		return 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 (matchHeaderForPattern(headerName, pattern)) {
					return true;
				}
			}
		}
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(MessageFormat.format("headerName=[{0}] WILL NOT be mapped", headerName));
		}
		return false;
	}

	private boolean matchHeaderForPattern(String headerName, String pattern) {
		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;
		}
		return false;
	}

	@Nullable
	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 static boolean containsElementIgnoreCase(String[] headerNames, String name) {
		for (String headerName : headerNames) {
			if (headerName.equalsIgnoreCase(name)) {
				return true;
			}
		}
		return false;
	}

	protected static long getFirstDate(String headerValue, String headerName) {
		for (DateTimeFormatter dateFormatter : DATE_FORMATS) {
			try {
				return ZonedDateTime.parse(headerValue, dateFormatter)
						.toInstant()
						.toEpochMilli();
			}
			catch (@SuppressWarnings(UNUSED) DateTimeParseException ex) {
				// ignore
			}
		}

		throw new IllegalArgumentException("Cannot parse date value '" + headerValue + "' for '" + headerName
				+ "' header");
	}

	/**
	 * 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);
	}

}
