/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.camel.component.http;

import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import javax.net.ssl.HostnameVerifier;

import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Endpoint;
import org.apache.camel.Producer;
import org.apache.camel.SSLContextParametersAware;
import org.apache.camel.http.base.HttpHelper;
import org.apache.camel.http.common.HttpBinding;
import org.apache.camel.http.common.HttpCommonComponent;
import org.apache.camel.http.common.HttpConfiguration;
import org.apache.camel.http.common.HttpRestHeaderFilterStrategy;
import org.apache.camel.spi.BeanIntrospection;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RestProducerFactory;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.PropertyBindingSupport;
import org.apache.camel.support.RestProducerFactoryHelper;
import org.apache.camel.support.http.HttpUtil;
import org.apache.camel.support.jsse.SSLContextParameters;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.PropertiesHelper;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.util.UnsafeUriCharactersEncoder;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Defines the HTTP Component
 */
@Metadata(label = "verifiers", enums = "parameters,connectivity")
@Component("http,https")
public class HttpComponent extends HttpCommonComponent implements RestProducerFactory, SSLContextParametersAware {

    private static final Logger LOG = LoggerFactory.getLogger(HttpComponent.class);

    @Metadata(label = "advanced",
              description = "To use the custom HttpClientConfigurer to perform configuration of the HttpClient that will be used.")
    protected HttpClientConfigurer httpClientConfigurer;
    @Metadata(label = "advanced", description = "To use a custom and shared HttpClientConnectionManager to manage connections."
                                                + " If this has been configured then this is always used for all endpoints created by this component.")
    protected HttpClientConnectionManager clientConnectionManager;
    @Metadata(label = "advanced",
              description = "To use a custom org.apache.hc.core5.http.protocol.HttpContext when executing requests.")
    protected HttpContext httpContext;
    @Metadata(label = "security", description = "To configure security using SSLContextParameters."
                                                + " Important: Only one instance of org.apache.camel.support.jsse.SSLContextParameters is supported per HttpComponent."
                                                + " If you need to use 2 or more different instances, you need to define a new HttpComponent per instance you need.")
    protected SSLContextParameters sslContextParameters;
    @Metadata(label = "security",
              description = "To use a custom X509HostnameVerifier such as DefaultHostnameVerifier or NoopHostnameVerifier.")
    protected HostnameVerifier x509HostnameVerifier = new DefaultHostnameVerifier();
    @Metadata(label = "advanced", defaultValue = "false",
              description = "To use System Properties as fallback for configuration for configuring HTTP Client")
    private boolean useSystemProperties;
    @Metadata(label = "producer,advanced", description = "To use a custom org.apache.hc.client5.http.cookie.CookieStore."
                                                         + " By default the org.apache.hc.client5.http.cookie.BasicCookieStore is used which is an in-memory only cookie store."
                                                         + " Notice if bridgeEndpoint=true then the cookie store is forced to be a noop cookie store as cookie"
                                                         + " shouldn't be stored as we are just bridging (eg acting as a proxy).")
    protected CookieStore cookieStore;

    // timeout
    @Metadata(label = "timeout", defaultValue = "" + 3 * 60 * 1000,
              description = "Returns the connection lease request timeout (in millis) used when requesting"
                            + " a connection from the connection manager."
                            + " A timeout value of zero is interpreted as a disabled timeout.")
    protected long connectionRequestTimeout = 3 * 60 * 1000L;
    @Metadata(label = "timeout", defaultValue = "" + 3 * 60 * 1000,
              description = "Determines the timeout (in millis) until a new connection is fully established."
                            + " A timeout value of zero is interpreted as an infinite timeout.")
    protected long connectTimeout = 3 * 60 * 1000L;
    @Metadata(label = "timeout", defaultValue = "" + 3 * 60 * 1000,
              description = "Determines the default socket timeout (in millis) value for blocking I/O operations.")
    protected long soTimeout = 3 * 60 * 1000L;
    @Metadata(label = "timeout",
              description = "Determines the timeout (in millis) until arrival of a response from the opposite"
                            + " endpoint. A timeout value of zero is interpreted as an infinite timeout."
                            + " Please note that response timeout may be unsupported by HTTP transports "
                            + "with message multiplexing.")
    protected long responseTimeout;

    // proxy
    @Metadata(label = "producer,proxy", description = "Proxy server host")
    protected String proxyHost;
    @Metadata(label = "producer,proxy", description = "Proxy server port")
    protected Integer proxyPort;
    @Metadata(label = "producer,proxy", description = "Comma-separated list of hosts that should bypass the proxy. "
                                                      + "Supports wildcards, e.g., localhost,*.example.com,192.168.*.")
    protected String nonProxyHosts;
    @Metadata(label = "producer,proxy", enums = "http,https",
              description = "Proxy server authentication protocol scheme to use")
    protected String proxyAuthScheme;
    @Metadata(label = "producer,proxy", enums = "Basic,Digest,NTLM",
              description = "Proxy authentication method to use (NTLM is deprecated)")
    protected String proxyAuthMethod;
    @Metadata(label = "producer,proxy", security = "secret", description = "Proxy server username")
    protected String proxyAuthUsername;
    @Metadata(label = "producer,proxy", security = "secret", description = "Proxy server password")
    protected String proxyAuthPassword;
    @Deprecated
    @Metadata(label = "producer,proxy", description = "Proxy server host")
    protected String proxyAuthHost;
    @Deprecated
    @Metadata(label = "producer,proxy", description = "Proxy server port")
    protected Integer proxyAuthPort;
    @Deprecated
    @Metadata(label = "producer,proxy", description = "Proxy authentication domain to use with NTLM")
    protected String proxyAuthDomain;
    @Deprecated
    @Metadata(label = "producer,proxy",
              description = "Proxy authentication domain (workstation name) to use with NTLM (NTLM is deprecated)")
    protected String proxyAuthNtHost;

    // options to the default created http connection manager
    @Metadata(label = "advanced", defaultValue = "200", description = "The maximum number of connections.")
    protected int maxTotalConnections = 200;
    @Metadata(label = "advanced", defaultValue = "20", description = "The maximum number of connections per route.")
    protected int connectionsPerRoute = 20;
    // It's MILLISECONDS, the default value is always keepAlive
    @Metadata(label = "advanced",
              description = "The time for connection to live, the time unit is millisecond, the default value is always keepAlive.")
    protected long connectionTimeToLive = -1;
    @Metadata(label = "security", defaultValue = "false", description = "Enable usage of global SSL context parameters.")
    protected boolean useGlobalSslContextParameters;
    @Metadata(label = "producer,advanced", defaultValue = "8192",
              description = "This threshold in bytes controls whether the response payload"
                            + " should be stored in memory as a byte array or be streaming based. Set this to -1 to always use streaming mode.")
    protected int responsePayloadStreamingThreshold = 8192;
    @Metadata(label = "advanced", description = "Disables automatic redirect handling")
    protected boolean redirectHandlingDisabled;
    @Metadata(label = "advanced",
              description = "Disables automatic request recovery and re-execution."
                            + " This is useful when a server responds with HTTP 429 (Too Many Requests) and includes a long Retry-After header,"
                            + " which would otherwise cause the client to wait (and appear to hang) before retrying.")
    protected boolean automaticRetriesDisabled;
    @Metadata(label = "advanced", description = "Disables automatic content decompression")
    protected boolean contentCompressionDisabled;
    @Metadata(label = "advanced", description = "Disables state (cookie) management")
    protected boolean cookieManagementDisabled;
    @Metadata(label = "advanced", description = "Disables authentication scheme caching")
    protected boolean authCachingDisabled;
    @Metadata(label = "advanced", description = "Disables connection state tracking")
    protected boolean connectionStateDisabled;
    @Metadata(label = "advanced",
              description = "Disables the default user agent set by this builder if none has been provided by the user")
    protected boolean defaultUserAgentDisabled;
    @Metadata(label = "producer",
              description = "Whether to skip Camel control headers (CamelHttp... headers) to influence this endpoint. Control headers from previous HTTP components can influence"
                            + " how this Camel component behaves such as CamelHttpPath, CamelHttpQuery, etc.")
    private boolean skipControlHeaders;
    @Metadata(label = "producer",
              description = "Whether to skip mapping all the Camel headers as HTTP request headers."
                            + " This is useful when you know that calling the HTTP service should not include any custom headers.")
    protected boolean skipRequestHeaders;
    @Metadata(label = "producer",
              description = "Whether to skip mapping all the HTTP response headers to Camel headers.")
    protected boolean skipResponseHeaders;
    @Metadata(label = "producer,advanced",
              defaultValue = "true",
              description = "If this option is true then IN exchange headers will be copied to OUT exchange headers according to copy strategy."
                            + " Setting this to false, allows to only include the headers from the HTTP response (not propagating IN headers).")
    protected boolean copyHeaders = true;
    @Metadata(label = "producer,advanced", defaultValue = "false",
              description = "Whether to the HTTP request should follow redirects."
                            + " By default the HTTP request does not follow redirects ")
    protected boolean followRedirects;
    @Metadata(label = "producer,advanced", description = "To set a custom HTTP User-Agent request header")
    protected String userAgent;
    @Metadata(label = "producer,advanced", autowired = true, description = "To use a custom activity listener")
    protected HttpActivityListener httpActivityListener;
    @Metadata(label = "producer",
              description = "To enable logging HTTP request and response. You can use a custom LoggingHttpActivityListener as httpActivityListener to control logging options.")
    protected boolean logHttpActivity;
    @Metadata(label = "producer,advanced", defaultValue = "true",
              description = "Whether the Content-Type header should automatic include charset for string based content.")
    protected boolean contentTypeCharsetEnabled = true;

    public HttpComponent() {
    }

    /**
     * Creates the HttpClientConfigurer based on the given parameters
     *
     * @param  parameters the map of parameters
     * @param  secure     whether the endpoint is secure (e.g., https)
     * @return            the configurer
     * @throws Exception  is thrown if error creating configurer
     */
    protected HttpClientConfigurer createHttpClientConfigurer(Map<String, Object> parameters, boolean secure) throws Exception {
        // prefer to use endpoint configured over component configured
        HttpClientConfigurer configurer
                = resolveAndRemoveReferenceParameter(parameters, "httpClientConfigurer", HttpClientConfigurer.class);
        if (configurer == null) {
            // fallback to component configured
            configurer = getHttpClientConfigurer();
        }
        HttpCredentialsHelper credentialsProvider = new HttpCredentialsHelper();
        configurer = configureBasicAuthentication(parameters, configurer, credentialsProvider);
        configurer = configureHttpProxy(parameters, configurer, secure, credentialsProvider);
        configurer = configureOAuth2Authentication(parameters, configurer);

        return configurer;
    }

    private HttpClientConfigurer configureOAuth2Authentication(
            Map<String, Object> parameters, HttpClientConfigurer configurer) {

        String clientId = getParameter(parameters, "oauth2ClientId", String.class);
        String clientSecret = getParameter(parameters, "oauth2ClientSecret", String.class);
        String tokenEndpoint = getParameter(parameters, "oauth2TokenEndpoint", String.class);
        String scope = getParameter(parameters, "oauth2Scope", String.class);
        String resourceIndicator = getParameter(parameters, "oauth2ResourceIndicator", String.class);
        HttpConfiguration configDefaults = new HttpConfiguration();
        boolean cacheTokens = getParameter(
                parameters,
                "oauth2CacheTokens",
                boolean.class,
                configDefaults.isOauth2CacheTokens());
        long cachedTokensDefaultExpirySeconds = getParameter(
                parameters,
                "oauth2CachedTokensDefaultExpirySeconds",
                long.class,
                configDefaults.getOauth2CachedTokensDefaultExpirySeconds());
        long cachedTokensExpirationMarginSeconds = getParameter(
                parameters,
                "oauth2CachedTokensExpirationMarginSeconds",
                long.class,
                configDefaults.getOauth2CachedTokensExpirationMarginSeconds());
        boolean useBodyAuthentication = getParameter(
                parameters,
                "oauth2BodyAuthentication",
                boolean.class,
                configDefaults.isOauth2BodyAuthentication());
        if (clientId != null && clientSecret != null && tokenEndpoint != null) {
            return CompositeHttpConfigurer.combineConfigurers(configurer,
                    new OAuth2ClientConfigurer(
                            clientId,
                            clientSecret,
                            tokenEndpoint,
                            resourceIndicator,
                            scope,
                            cacheTokens,
                            cachedTokensDefaultExpirySeconds,
                            cachedTokensExpirationMarginSeconds,
                            useBodyAuthentication));
        }
        return configurer;
    }

    private HttpClientConfigurer configureBasicAuthentication(
            Map<String, Object> parameters, HttpClientConfigurer configurer,
            HttpCredentialsHelper credentialsProvider) {
        String authUsername = getParameter(parameters, "authUsername", String.class);
        String authPassword = getParameter(parameters, "authPassword", String.class);

        if (authUsername != null && authPassword != null) {
            String authDomain = getParameter(parameters, "authDomain", String.class);
            String authHost = getParameter(parameters, "authHost", String.class);

            return CompositeHttpConfigurer.combineConfigurers(configurer,
                    new DefaultAuthenticationHttpClientConfigurer(
                            authUsername, authPassword, authDomain, authHost, null, credentialsProvider));
        } else if (this.httpConfiguration != null) {
            if ("basic".equalsIgnoreCase(this.httpConfiguration.getAuthMethod())
                    || "bearer".equalsIgnoreCase(this.httpConfiguration.getAuthMethod())) {
                return CompositeHttpConfigurer.combineConfigurers(configurer,
                        new DefaultAuthenticationHttpClientConfigurer(
                                this.httpConfiguration.getAuthUsername(),
                                this.httpConfiguration.getAuthPassword(), this.httpConfiguration.getAuthDomain(),
                                this.httpConfiguration.getAuthHost(), this.httpConfiguration.getAuthBearerToken(),
                                credentialsProvider));
            }
        }

        return configurer;
    }

    private HttpClientConfigurer configureHttpProxy(
            Map<String, Object> parameters, HttpClientConfigurer configurer, boolean secure,
            HttpCredentialsHelper credentialsProvider) {

        String nonProxyhosts = getParameter(parameters, "nonProxyHosts", String.class, getNonProxyHosts());
        String proxyAuthScheme = getParameter(parameters, "proxyAuthScheme", String.class, getProxyAuthScheme());
        if (proxyAuthScheme == null) {
            // fallback and use either http or https depending on secure
            proxyAuthScheme = secure ? "https" : "http";
        }
        // these are old names and are deprecated
        String proxyAuthHost = getParameter(parameters, "proxyAuthHost", String.class, getProxyAuthHost());
        Integer proxyAuthPort = getParameter(parameters, "proxyAuthPort", Integer.class, getProxyAuthPort());
        if (proxyAuthHost == null) {
            proxyAuthHost = getParameter(parameters, "proxyHost", String.class, getProxyHost());
        }
        if (proxyAuthPort == null) {
            proxyAuthPort = getParameter(parameters, "proxyPort", Integer.class, getProxyPort());
        }

        if (proxyAuthHost != null && proxyAuthPort != null) {
            String proxyAuthUsername = getParameter(parameters, "proxyAuthUsername", String.class, getProxyAuthUsername());
            String proxyAuthPassword = getParameter(parameters, "proxyAuthPassword", String.class, getProxyAuthPassword());
            String proxyAuthDomain = getParameter(parameters, "proxyAuthDomain", String.class, getProxyAuthDomain());
            String proxyAuthNtHost = getParameter(parameters, "proxyAuthNtHost", String.class, getProxyAuthNtHost());

            LOG.debug("Configuring HTTP client to use HTTP proxy {}:{}", proxyAuthHost, proxyAuthPort);

            if (proxyAuthUsername != null && proxyAuthPassword != null) {
                return CompositeHttpConfigurer.combineConfigurers(
                        configurer,
                        new ProxyHttpClientConfigurer(
                                proxyAuthHost, proxyAuthPort, proxyAuthScheme, proxyAuthUsername, proxyAuthPassword,
                                proxyAuthDomain, proxyAuthNtHost, credentialsProvider, nonProxyhosts));
            } else {
                return CompositeHttpConfigurer.combineConfigurers(configurer,
                        new ProxyHttpClientConfigurer(proxyAuthHost, proxyAuthPort, proxyAuthScheme, nonProxyhosts));
            }
        }

        return configurer;
    }

    @Override
    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
        Map<String, Object> httpClientParameters = new HashMap<>(parameters);
        final Map<String, Object> httpClientOptions = new HashMap<>();

        // timeout values can be configured on both component and endpoint level, where endpoint takes priority
        Timeout valConnectionRequestTimeout
                = getAndRemoveParameter(parameters, "connectionRequestTimeout", Timeout.class,
                        Timeout.ofMilliseconds(connectionRequestTimeout));
        if (!Timeout.ofMinutes(3).equals(valConnectionRequestTimeout)) {
            httpClientOptions.put("connectionRequestTimeout", valConnectionRequestTimeout);
        }
        Timeout valResponseTimeout
                = getAndRemoveParameter(parameters, "responseTimeout", Timeout.class, Timeout.ofMilliseconds(responseTimeout));
        if (!Timeout.ofMilliseconds(0).equals(valResponseTimeout)) {
            httpClientOptions.put("responseTimeout", valResponseTimeout);
        }
        Timeout valConnectTimeout
                = getAndRemoveParameter(parameters, "connectTimeout", Timeout.class, Timeout.ofMilliseconds(connectTimeout));
        if (!Timeout.ofMinutes(3).equals(valConnectTimeout)) {
            httpClientOptions.put("connectTimeout", valConnectTimeout);
        }
        final Map<String, Object> httpConnectionOptions = new HashMap<>();
        Timeout valSoTimeout = getAndRemoveParameter(parameters, "soTimeout", Timeout.class, Timeout.ofMilliseconds(soTimeout));
        if (!Timeout.ofMinutes(3).equals(valSoTimeout)) {
            httpConnectionOptions.put("soTimeout", valSoTimeout);
        }

        HttpBinding httpBinding = resolveAndRemoveReferenceParameter(parameters, "httpBinding", HttpBinding.class);
        HttpContext httpContext = resolveAndRemoveReferenceParameter(parameters, "httpContext", HttpContext.class);

        SSLContextParameters sslContextParameters
                = resolveAndRemoveReferenceParameter(parameters, "sslContextParameters", SSLContextParameters.class);
        if (sslContextParameters == null) {
            sslContextParameters = getSslContextParameters();
        }
        if (sslContextParameters == null) {
            // only secure (https) should use global SSL
            boolean secure = HttpHelper.isSecureConnection(uri);
            if (secure) {
                sslContextParameters = retrieveGlobalSslContextParameters();
            }
        }

        String httpMethodRestrict = getAndRemoveParameter(parameters, "httpMethodRestrict", String.class);
        boolean muteException = getAndRemoveParameter(parameters, "muteException", boolean.class, isMuteException());

        HeaderFilterStrategy headerFilterStrategy
                = resolveAndRemoveReferenceParameter(parameters, "headerFilterStrategy", HeaderFilterStrategy.class);

        // the actual protocol if present in the remainder part should take precedence
        String secureProtocol = uri;
        if (remaining.startsWith("http:") || remaining.startsWith("https:")) {
            secureProtocol = remaining;
        }
        boolean secure = HttpHelper.isSecureConnection(secureProtocol) || sslContextParameters != null;

        // the remaining part should be without protocol as that was how this component was originally created
        remaining = org.apache.camel.component.http.HttpUtil.removeHttpOrHttpsProtocol(remaining);

        // need to set the scheme on address uri depending on if it's secure or not
        String addressUri = (secure ? "https://" : "http://") + remaining;

        addressUri = UnsafeUriCharactersEncoder.encodeHttpURI(addressUri);
        URI uriHttpUriAddress = new URI(addressUri);

        // the endpoint uri should use the component name as the scheme, so we need to re-create it once more
        String scheme = StringHelper.before(uri, "://");

        // uri part should be without protocol as that was how this component was originally created
        uri = org.apache.camel.component.http.HttpUtil.removeHttpOrHttpsProtocol(uri);

        // create the configurer to use for this endpoint
        HttpClientConfigurer configurer = createHttpClientConfigurer(parameters, secure);
        URI endpointUri = URISupport.createRemainingURI(uriHttpUriAddress, httpClientParameters);

        endpointUri = URISupport.createRemainingURI(
                new URI(
                        scheme,
                        endpointUri.getUserInfo(),
                        endpointUri.getHost(),
                        endpointUri.getPort(),
                        endpointUri.getPath(),
                        endpointUri.getQuery(),
                        endpointUri.getFragment()),
                httpClientParameters);

        // create the endpoint and set the http uri to be null
        String endpointUriString = endpointUri.toString();

        LOG.debug("Creating endpoint uri {}", endpointUriString);
        final HttpClientConnectionManager localConnectionManager
                = createConnectionManager(parameters, sslContextParameters, httpConnectionOptions);
        final HttpClientBuilder clientBuilder = createHttpClientBuilder(uri, parameters, httpClientOptions);
        HttpEndpoint endpoint = new HttpEndpoint(endpointUriString, this, clientBuilder, localConnectionManager, configurer);
        endpoint.setResponseTimeout(valResponseTimeout.toMilliseconds());
        endpoint.setSoTimeout(valSoTimeout.toMilliseconds());
        endpoint.setConnectTimeout(valConnectTimeout.toMilliseconds());
        endpoint.setConnectionRequestTimeout(valConnectionRequestTimeout.toMilliseconds());
        endpoint.setCopyHeaders(copyHeaders);
        endpoint.setSkipControlHeaders(skipControlHeaders);
        endpoint.setSkipRequestHeaders(skipRequestHeaders);
        endpoint.setSkipResponseHeaders(skipResponseHeaders);
        endpoint.setUserAgent(userAgent);
        endpoint.setMuteException(muteException);
        endpoint.setHttpActivityListener(httpActivityListener);
        endpoint.setLogHttpActivity(logHttpActivity);
        endpoint.setContentTypeCharsetEnabled(contentTypeCharsetEnabled);
        endpoint.setUseSystemProperties(this.useSystemProperties);

        // configure the endpoint with the common configuration from the component
        if (getHttpConfiguration() != null) {
            Map<String, Object> properties = new HashMap<>();
            BeanIntrospection beanIntrospection = PluginHelper.getBeanIntrospection(getCamelContext());
            beanIntrospection.getProperties(getHttpConfiguration(), properties, null);
            setProperties(endpoint, properties);
        }

        // configure the endpoint
        setProperties(endpoint, parameters);

        // we cannot change the port of an URI, we must create a new one with an explicit port value
        URI httpUri = URISupport.createRemainingURI(
                new URI(
                        uriHttpUriAddress.getScheme(),
                        uriHttpUriAddress.getUserInfo(),
                        uriHttpUriAddress.getHost(),
                        uriHttpUriAddress.getPort(),
                        uriHttpUriAddress.getPath(),
                        uriHttpUriAddress.getQuery(),
                        uriHttpUriAddress.getFragment()),
                parameters);

        endpoint.setHttpUri(httpUri);

        if (headerFilterStrategy != null) {
            endpoint.setHeaderFilterStrategy(headerFilterStrategy);
        } else {
            setEndpointHeaderFilterStrategy(endpoint);
        }
        endpoint.setHttpBinding(getHttpBinding());
        if (httpBinding != null) {
            endpoint.setHttpBinding(httpBinding);
        }
        if (httpMethodRestrict != null) {
            endpoint.setHttpMethodRestrict(httpMethodRestrict);
        }
        endpoint.setHttpContext(getHttpContext());
        if (httpContext != null) {
            endpoint.setHttpContext(httpContext);
        }
        if (endpoint.getCookieStore() == null) {
            endpoint.setCookieStore(getCookieStore());
        }
        endpoint.setHttpClientOptions(httpClientOptions);
        endpoint.setHttpConnectionOptions(httpConnectionOptions);
        return endpoint;
    }

    protected HttpClientConnectionManager createConnectionManager(
            final Map<String, Object> parameters,
            final SSLContextParameters sslContextParameters, Map<String, Object> httpConnectionOptions)
            throws GeneralSecurityException, IOException {
        if (clientConnectionManager != null) {
            return clientConnectionManager;
        }

        final HostnameVerifier resolvedHostnameVerifier
                = resolveAndRemoveReferenceParameter(parameters, "x509HostnameVerifier", HostnameVerifier.class);
        final HostnameVerifier hostnameVerifier = Optional.ofNullable(resolvedHostnameVerifier).orElse(x509HostnameVerifier);

        // need to check the parameters of maxTotalConnections and connectionsPerRoute
        final int maxTotalConnections = getAndRemoveParameter(parameters, "maxTotalConnections", int.class, 0);
        final int connectionsPerRoute = getAndRemoveParameter(parameters, "connectionsPerRoute", int.class, 0);
        // do not remove as we set this later again
        final boolean sysProp = getParameter(parameters, "useSystemProperties", boolean.class, useSystemProperties);
        final TlsSocketStrategy tlsStrategy = createTlsStrategy(hostnameVerifier, sslContextParameters, sysProp);

        // allow the builder pattern
        httpConnectionOptions.putAll(PropertiesHelper.extractProperties(parameters, "httpConnection."));
        SocketConfig.Builder socketConfigBuilder = SocketConfig.custom();
        PropertyBindingSupport.bindProperties(getCamelContext(), socketConfigBuilder, httpConnectionOptions);

        return createConnectionManager(tlsStrategy, maxTotalConnections, connectionsPerRoute, socketConfigBuilder.build());
    }

    protected HttpClientBuilder createHttpClientBuilder(
            final String uri, final Map<String, Object> parameters,
            final Map<String, Object> httpClientOptions) {
        // http client can be configured from URI options
        HttpClientBuilder clientBuilder = HttpClientBuilder.create();
        // allow the builder pattern
        httpClientOptions.putAll(PropertiesHelper.extractProperties(parameters, "httpClient."));
        PropertyBindingSupport.bindProperties(getCamelContext(), clientBuilder, httpClientOptions);
        // set the Request configure this way and allow the builder pattern
        RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
        PropertyBindingSupport.bindProperties(getCamelContext(), requestConfigBuilder, httpClientOptions);
        clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());

        // validate that we could resolve all httpClient. parameters as this component is lenient
        validateParameters(uri, httpClientOptions, null);

        // endpoint parameter can override component level
        boolean fr = getParameter(parameters, "followRedirects", Boolean.class, followRedirects);

        if (redirectHandlingDisabled || !fr) {
            clientBuilder.disableRedirectHandling();
        }
        if (automaticRetriesDisabled) {
            clientBuilder.disableAutomaticRetries();
        }
        if (contentCompressionDisabled) {
            clientBuilder.disableContentCompression();
        }
        if (cookieManagementDisabled) {
            clientBuilder.disableCookieManagement();
        }
        if (authCachingDisabled) {
            clientBuilder.disableAuthCaching();
        }
        if (connectionStateDisabled) {
            clientBuilder.disableConnectionState();
        }
        if (defaultUserAgentDisabled) {
            clientBuilder.disableDefaultUserAgent();
        }
        if (fr) {
            clientBuilder.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE);
        }

        return clientBuilder;
    }

    protected TlsSocketStrategy createTlsStrategy(
            HostnameVerifier x509HostnameVerifier,
            SSLContextParameters sslContextParams, boolean useSystemProperties)
            throws GeneralSecurityException, IOException {
        // create the TLS strategy to use
        if (sslContextParams != null) {
            return new DefaultClientTlsStrategy(sslContextParams.createSSLContext(getCamelContext()), x509HostnameVerifier);
        } else {
            return new DefaultClientTlsStrategy(
                    useSystemProperties ? SSLContexts.createSystemDefault() : SSLContexts.createDefault(),
                    x509HostnameVerifier);
        }
    }

    protected HttpClientConnectionManager createConnectionManager(
            TlsSocketStrategy tlsStrategy,
            int maxTotalConnections, int connectionsPerRoute, SocketConfig defaultSocketConfig) {
        // set up the connection using the builder pattern
        ConnectionConfig connConfig = ConnectionConfig.custom()
                .setTimeToLive(TimeValue.ofMilliseconds(getConnectionTimeToLive()))
                .build();
        PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create()
                .setTlsSocketStrategy(tlsStrategy)
                .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
                .setDefaultConnectionConfig(connConfig)
                .setDefaultSocketConfig(defaultSocketConfig);

        int localMaxTotalConnections = maxTotalConnections;
        if (localMaxTotalConnections == 0) {
            localMaxTotalConnections = getMaxTotalConnections();
        }
        if (localMaxTotalConnections > 0) {
            builder.setMaxConnTotal(localMaxTotalConnections);
        }

        int localConnectionsPerRoute = connectionsPerRoute;
        if (localConnectionsPerRoute == 0) {
            localConnectionsPerRoute = getConnectionsPerRoute();
        }
        if (localConnectionsPerRoute > 0) {
            builder.setMaxConnPerRoute(localConnectionsPerRoute);
        }

        PoolingHttpClientConnectionManager answer = builder.build();
        LOG.debug("Created ClientConnectionManager {}", answer);

        return answer;
    }

    @Override
    protected boolean useIntrospectionOnEndpoint() {
        return false;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Producer createProducer(
            CamelContext camelContext, String host,
            String verb, String basePath, String uriTemplate, String queryParameters,
            String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters)
            throws Exception {

        // avoid leading slash
        basePath = FileUtil.stripLeadingSeparator(basePath);
        uriTemplate = FileUtil.stripLeadingSeparator(uriTemplate);

        // get the endpoint
        String url = host;
        if (!ObjectHelper.isEmpty(basePath)) {
            url += "/" + basePath;
        }
        if (!ObjectHelper.isEmpty(uriTemplate)) {
            url += "/" + uriTemplate;
        }

        RestConfiguration config = configuration;
        if (config == null) {
            config = CamelContextHelper.getRestConfiguration(getCamelContext(), null, "http");
        }

        Map<String, Object> map = new HashMap<>();
        // build query string, and append any endpoint configuration properties
        if (config.getProducerComponent() == null || config.getProducerComponent().equals("http")) {
            // setup endpoint options
            map.put("httpMethod", verb);
            if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) {
                map.putAll(config.getEndpointProperties());
            }
        }

        url = HttpUtil.recreateUrl(map, url);

        parameters = parameters != null ? new HashMap<>(parameters) : new HashMap<>();

        // there are cases where we might end up here without component being created beforehand
        // we need to abide by the component properties specified in the parameters when creating
        // the component, one such case is when we switch from "http" to "https" component name
        RestProducerFactoryHelper.setupComponentFor(url, camelContext, (Map<String, Object>) parameters.remove("component"));

        HttpEndpoint endpoint = (HttpEndpoint) camelContext.getEndpoint(url, parameters);

        String path = uriTemplate != null ? uriTemplate : basePath;

        HeaderFilterStrategy headerFilterStrategy
                = resolveAndRemoveReferenceParameter(parameters, "headerFilterStrategy", HeaderFilterStrategy.class);
        if (headerFilterStrategy != null) {
            endpoint.setHeaderFilterStrategy(headerFilterStrategy);
        } else {
            endpoint.setHeaderFilterStrategy(new HttpRestHeaderFilterStrategy(path, queryParameters));
        }
        // the endpoint must be started before creating the producer
        ServiceHelper.startService(endpoint);

        return endpoint.createProducer();
    }

    public HttpClientConfigurer getHttpClientConfigurer() {
        return httpClientConfigurer;
    }

    /**
     * To use the custom HttpClientConfigurer to perform configuration of the HttpClient that will be used.
     */
    public void setHttpClientConfigurer(HttpClientConfigurer httpClientConfigurer) {
        this.httpClientConfigurer = httpClientConfigurer;
    }

    public HttpClientConnectionManager getClientConnectionManager() {
        return clientConnectionManager;
    }

    /**
     * To use a custom and shared HttpClientConnectionManager to manage connections. If this has been configured, then
     * this is always used for all endpoints created by this component.
     */
    public void setClientConnectionManager(HttpClientConnectionManager clientConnectionManager) {
        this.clientConnectionManager = clientConnectionManager;
    }

    public HttpContext getHttpContext() {
        return httpContext;
    }

    /**
     * To use a custom HttpContext when executing requests.
     */
    public void setHttpContext(HttpContext httpContext) {
        this.httpContext = httpContext;
    }

    public SSLContextParameters getSslContextParameters() {
        return sslContextParameters;
    }

    /**
     * To configure security using SSLContextParameters. Important: Only one instance of
     * org.apache.camel.support.jsse.SSLContextParameters is supported per HttpComponent. If you need to use two or more
     * different instances, you need to define a new HttpComponent per instance.
     */
    public void setSslContextParameters(SSLContextParameters sslContextParameters) {
        this.sslContextParameters = sslContextParameters;
    }

    @Override
    public boolean isUseGlobalSslContextParameters() {
        return this.useGlobalSslContextParameters;
    }

    /**
     * Enable usage of global SSL context parameters.
     */
    @Override
    public void setUseGlobalSslContextParameters(boolean useGlobalSslContextParameters) {
        this.useGlobalSslContextParameters = useGlobalSslContextParameters;
    }

    public HostnameVerifier getX509HostnameVerifier() {
        return x509HostnameVerifier;
    }

    /**
     * To use a custom X509HostnameVerifier such as DefaultHostnameVerifier or NoopHostnameVerifier.
     */
    public void setX509HostnameVerifier(HostnameVerifier x509HostnameVerifier) {
        this.x509HostnameVerifier = x509HostnameVerifier;
    }

    public boolean isUseSystemProperties() {
        return useSystemProperties;
    }

    public void setUseSystemProperties(boolean useSystemProperties) {
        this.useSystemProperties = useSystemProperties;
    }

    public int getMaxTotalConnections() {
        return maxTotalConnections;
    }

    /**
     * The maximum number of connections.
     */
    public void setMaxTotalConnections(int maxTotalConnections) {
        this.maxTotalConnections = maxTotalConnections;
    }

    public int getConnectionsPerRoute() {
        return connectionsPerRoute;
    }

    /**
     * The maximum number of connections per route.
     */
    public void setConnectionsPerRoute(int connectionsPerRoute) {
        this.connectionsPerRoute = connectionsPerRoute;
    }

    public long getConnectionTimeToLive() {
        return connectionTimeToLive;
    }

    /**
     * The time for connection to live, the time unit is millisecond; the default value is always keepAlive.
     */
    public void setConnectionTimeToLive(long connectionTimeToLive) {
        this.connectionTimeToLive = connectionTimeToLive;
    }

    public CookieStore getCookieStore() {
        return cookieStore;
    }

    /**
     * To use a custom CookieStore. By default, the BasicCookieStore is used which is an in-memory only cookie store.
     * Notice if bridgeEndpoint=true then the cookie store is forced to be a noop cookie store as cookie shouldn't be
     * stored as we are just bridging (e.g., acting as a proxy).
     */
    public void setCookieStore(CookieStore cookieStore) {
        this.cookieStore = cookieStore;
    }

    public long getConnectionRequestTimeout() {
        return connectionRequestTimeout;
    }

    /**
     * Returns the connection lease request timeout used when requesting a connection from the connection manager.
     * <p>
     * A timeout value of zero is interpreted as a disabled timeout.
     * </p>
     * <p>
     * Default: 3 minutes
     * </p>
     */
    public void setConnectionRequestTimeout(long connectionRequestTimeout) {
        this.connectionRequestTimeout = connectionRequestTimeout;
    }

    public long getConnectTimeout() {
        return connectTimeout;
    }

    /**
     * Determines the timeout until a new connection is fully established. This may also include transport security
     * negotiation exchanges such as {@code SSL} or {@code TLS} protocol negotiation.
     * <p>
     * A timeout value of zero is interpreted as an infinite timeout.
     * </p>
     * <p>
     * Default: 3 minutes
     * </p>
     */
    public void setConnectTimeout(long connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public long getSoTimeout() {
        return soTimeout;
    }

    /**
     * Determines the default socket timeout value for blocking I/O operations.
     * <p>
     * Default: 3 minutes
     * </p>
     */
    public void setSoTimeout(long soTimeout) {
        this.soTimeout = soTimeout;
    }

    public long getResponseTimeout() {
        return responseTimeout;
    }

    /**
     * Determines the timeout until arrival of a response from the opposite endpoint.
     * <p>
     * A timeout value of zero is interpreted as an infinite timeout.
     * </p>
     * <p>
     * Please note that response timeout may be unsupported by HTTP transports with message multiplexing.
     * </p>
     * <p>
     * Default: {@code 0}
     * </p>
     */
    public void setResponseTimeout(long responseTimeout) {
        this.responseTimeout = responseTimeout;
    }

    public String getProxyHost() {
        return proxyHost;
    }

    public void setProxyHost(String proxyHost) {
        this.proxyHost = proxyHost;
    }

    public Integer getProxyPort() {
        return proxyPort;
    }

    public void setProxyPort(Integer proxyPort) {
        this.proxyPort = proxyPort;
    }

    public String getProxyAuthScheme() {
        return proxyAuthScheme;
    }

    public void setProxyAuthScheme(String proxyAuthScheme) {
        this.proxyAuthScheme = proxyAuthScheme;
    }

    public String getProxyAuthMethod() {
        return proxyAuthMethod;
    }

    public void setProxyAuthMethod(String proxyAuthMethod) {
        this.proxyAuthMethod = proxyAuthMethod;
    }

    public String getProxyAuthUsername() {
        return proxyAuthUsername;
    }

    public void setProxyAuthUsername(String proxyAuthUsername) {
        this.proxyAuthUsername = proxyAuthUsername;
    }

    public String getProxyAuthPassword() {
        return proxyAuthPassword;
    }

    public void setProxyAuthPassword(String proxyAuthPassword) {
        this.proxyAuthPassword = proxyAuthPassword;
    }

    @Deprecated
    public String getProxyAuthHost() {
        return proxyAuthHost;
    }

    @Deprecated
    public void setProxyAuthHost(String proxyAuthHost) {
        this.proxyAuthHost = proxyAuthHost;
    }

    @Deprecated
    public Integer getProxyAuthPort() {
        return proxyAuthPort;
    }

    @Deprecated
    public void setProxyAuthPort(Integer proxyAuthPort) {
        this.proxyAuthPort = proxyAuthPort;
    }

    public String getProxyAuthDomain() {
        return proxyAuthDomain;
    }

    public void setProxyAuthDomain(String proxyAuthDomain) {
        this.proxyAuthDomain = proxyAuthDomain;
    }

    public String getProxyAuthNtHost() {
        return proxyAuthNtHost;
    }

    public void setProxyAuthNtHost(String proxyAuthNtHost) {
        this.proxyAuthNtHost = proxyAuthNtHost;
    }

    public int getResponsePayloadStreamingThreshold() {
        return responsePayloadStreamingThreshold;
    }

    public void setResponsePayloadStreamingThreshold(int responsePayloadStreamingThreshold) {
        this.responsePayloadStreamingThreshold = responsePayloadStreamingThreshold;
    }

    public boolean isRedirectHandlingDisabled() {
        return redirectHandlingDisabled;
    }

    public void setRedirectHandlingDisabled(boolean redirectHandlingDisabled) {
        this.redirectHandlingDisabled = redirectHandlingDisabled;
    }

    public boolean isAutomaticRetriesDisabled() {
        return automaticRetriesDisabled;
    }

    public void setAutomaticRetriesDisabled(boolean automaticRetriesDisabled) {
        this.automaticRetriesDisabled = automaticRetriesDisabled;
    }

    public boolean isContentCompressionDisabled() {
        return contentCompressionDisabled;
    }

    public void setContentCompressionDisabled(boolean contentCompressionDisabled) {
        this.contentCompressionDisabled = contentCompressionDisabled;
    }

    public boolean isCookieManagementDisabled() {
        return cookieManagementDisabled;
    }

    public void setCookieManagementDisabled(boolean cookieManagementDisabled) {
        this.cookieManagementDisabled = cookieManagementDisabled;
    }

    public boolean isAuthCachingDisabled() {
        return authCachingDisabled;
    }

    public void setAuthCachingDisabled(boolean authCachingDisabled) {
        this.authCachingDisabled = authCachingDisabled;
    }

    public boolean isConnectionStateDisabled() {
        return connectionStateDisabled;
    }

    public void setConnectionStateDisabled(boolean connectionStateDisabled) {
        this.connectionStateDisabled = connectionStateDisabled;
    }

    public boolean isDefaultUserAgentDisabled() {
        return defaultUserAgentDisabled;
    }

    public void setDefaultUserAgentDisabled(boolean defaultUserAgentDisabled) {
        this.defaultUserAgentDisabled = defaultUserAgentDisabled;
    }

    public boolean isCopyHeaders() {
        return copyHeaders;
    }

    public void setCopyHeaders(boolean copyHeaders) {
        this.copyHeaders = copyHeaders;
    }

    public boolean isSkipControlHeaders() {
        return skipControlHeaders;
    }

    public void setSkipControlHeaders(boolean skipControlHeaders) {
        this.skipControlHeaders = skipControlHeaders;
    }

    public boolean isSkipRequestHeaders() {
        return skipRequestHeaders;
    }

    public void setSkipRequestHeaders(boolean skipRequestHeaders) {
        this.skipRequestHeaders = skipRequestHeaders;
    }

    public boolean isSkipResponseHeaders() {
        return skipResponseHeaders;
    }

    public void setSkipResponseHeaders(boolean skipResponseHeaders) {
        this.skipResponseHeaders = skipResponseHeaders;
    }

    public boolean isFollowRedirects() {
        return followRedirects;
    }

    public void setFollowRedirects(boolean followRedirects) {
        this.followRedirects = followRedirects;
    }

    public String getUserAgent() {
        return userAgent;
    }

    public void setUserAgent(String userAgent) {
        this.userAgent = userAgent;
    }

    public HttpActivityListener getHttpActivityListener() {
        return httpActivityListener;
    }

    public void setHttpActivityListener(HttpActivityListener httpActivityListener) {
        this.httpActivityListener = httpActivityListener;
    }

    public boolean isLogHttpActivity() {
        return logHttpActivity;
    }

    public void setLogHttpActivity(boolean logHttpActivity) {
        this.logHttpActivity = logHttpActivity;
    }

    public boolean isContentTypeCharsetEnabled() {
        return contentTypeCharsetEnabled;
    }

    public void setContentTypeCharsetEnabled(boolean contentTypeCharsetEnabled) {
        this.contentTypeCharsetEnabled = contentTypeCharsetEnabled;
    }

    public String getNonProxyHosts() {
        return this.nonProxyHosts;
    }

    public void setNonProxyHosts(String nonProxyHosts) {
        this.nonProxyHosts = nonProxyHosts;
    }

    @Override
    public void doStart() throws Exception {
        super.doStart();
        if (logHttpActivity && httpActivityListener == null) {
            httpActivityListener = new LoggingHttpActivityListener();
        }
        CamelContextAware.trySetCamelContext(httpActivityListener, getCamelContext());
        ServiceHelper.startService(httpActivityListener);
    }

    @Override
    public void doStop() throws Exception {
        // shutdown connection manager
        if (clientConnectionManager != null) {
            LOG.info("Shutting down ClientConnectionManager: {}", clientConnectionManager);
            clientConnectionManager.close();
            clientConnectionManager = null;
        }
        ServiceHelper.stopService(httpActivityListener);

        super.doStop();
    }
}
