content: /*
   * 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.salesforce;
  
  import java.net.URI;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import java.util.function.Supplier;
  import java.util.regex.Pattern;
  
  import org.apache.avro.specific.SpecificRecord;
  import org.apache.camel.CamelContext;
  import org.apache.camel.Endpoint;
  import org.apache.camel.SSLContextParametersAware;
  import org.apache.camel.TypeConverter;
  import org.apache.camel.component.salesforce.api.SalesforceException;
  import org.apache.camel.component.salesforce.api.dto.AbstractDTOBase;
  import org.apache.camel.component.salesforce.api.utils.SecurityUtils;
  import org.apache.camel.component.salesforce.internal.OperationName;
  import org.apache.camel.component.salesforce.internal.SalesforceSession;
  import org.apache.camel.component.salesforce.internal.client.DefaultRawClient;
  import org.apache.camel.component.salesforce.internal.client.DefaultRestClient;
  import org.apache.camel.component.salesforce.internal.client.RawClient;
  import org.apache.camel.component.salesforce.internal.client.RestClient;
  import org.apache.camel.component.salesforce.internal.streaming.SubscriptionHelper;
  import org.apache.camel.spi.Metadata;
  import org.apache.camel.spi.annotations.Component;
  import org.apache.camel.support.DefaultComponent;
  import org.apache.camel.support.PluginHelper;
  import org.apache.camel.support.PropertyBindingSupport;
  import org.apache.camel.support.jsse.KeyStoreParameters;
  import org.apache.camel.support.jsse.SSLContextParameters;
  import org.apache.camel.support.service.ServiceHelper;
  import org.apache.camel.util.ObjectHelper;
  import org.apache.camel.util.StringHelper;
  import org.eclipse.jetty.client.Authentication;
  import org.eclipse.jetty.client.BasicAuthentication;
  import org.eclipse.jetty.client.DigestAuthentication;
  import org.eclipse.jetty.client.HttpProxy;
  import org.eclipse.jetty.client.Origin;
  import org.eclipse.jetty.client.ProxyConfiguration;
  import org.eclipse.jetty.client.Socks4Proxy;
  import org.eclipse.jetty.util.ssl.SslContextFactory;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.apache.camel.component.salesforce.SalesforceLoginConfig.DEFAULT_LOGIN_URL;
  
  /**
   * Represents the component that manages {@link SalesforceEndpoint}.
   */
  @Metadata(label = "verifiers", enums = "parameters,connectivity")
  @Component("salesforce")
  public class SalesforceComponent extends DefaultComponent implements SSLContextParametersAware {
  
      public static final String HTTP_PROXY_HOST = "httpProxyHost";
      public static final String HTTP_PROXY_PORT = "httpProxyPort";
      public static final String HTTP_PROXY_IS_SOCKS4 = "isHttpProxySocks4";
      public static final String HTTP_PROXY_IS_SECURE = "isHttpProxySecure";
      public static final String HTTP_PROXY_INCLUDE = "httpProxyInclude";
      public static final String HTTP_PROXY_EXCLUDE = "httpProxyExclude";
      public static final String HTTP_PROXY_USERNAME = "httpProxyUsername";
      public static final String HTTP_PROXY_PASSWORD = "httpProxyPassword";
      public static final String HTTP_PROXY_USE_DIGEST_AUTH = "httpProxyUseDigestAuth";
      public static final String HTTP_PROXY_AUTH_URI = "httpProxyAuthUri";
      public static final String HTTP_PROXY_REALM = "httpProxyRealm";
      public static final String HTTP_CONNECTION_TIMEOUT = "httpConnectionTimeout";
      public static final String HTTP_IDLE_TIMEOUT = "httpIdleTimeout";
      public static final String HTTP_REQUEST_TIMEOUT = "httpRequestTimeout";
      public static final String HTTP_MAX_CONTENT_LENGTH = "httpMaxContentLength";
      public static final String HTTP_REQUEST_BUFFER_SIZE = "httpRequestBufferSize";
  
      static final int CONNECTION_TIMEOUT = 60000;
      static final int REQUEST_TIMEOUT = 60000;
      static final int IDLE_TIMEOUT = 10000;
      static final int REQUEST_BUFFER_SIZE = 8192;
  
      static final Pattern SOBJECT_NAME_PATTERN = Pattern.compile("^.*[\\?&]sObjectName=([^&,]+).*$");
      static final String APEX_CALL_PREFIX = OperationName.APEX_CALL.value() + "/";
  
      private static final Logger LOG = LoggerFactory.getLogger(SalesforceComponent.class);
      private static final String SALESFORCE_EVENTBUS_PACKAGE = "com.sforce.eventbus";
  
      @Metadata(description = "All authentication configuration in one nested bean, all properties set there can be set"
                              + " directly on the component as well",
                label = "common,security")
      private SalesforceLoginConfig loginConfig;
  
      @Metadata(description = "URL of the Salesforce instance used after authentication, by default received from"
                              + " Salesforce on successful authentication",
                label = "common,security")
      private String instanceUrl;
  
      // allow fine grained login as well
      @Metadata(description = "URL of the Salesforce instance used for authentication, by default set to "
                              + DEFAULT_LOGIN_URL,
                label = "common,security", defaultValue = DEFAULT_LOGIN_URL, required = true)
      private String loginUrl;
  
      @Metadata(description = "OAuth Consumer Key of the connected app configured in the Salesforce instance setup."
                              + " Typically a connected app needs to be configured but one can be provided by installing a package.",
                label = "common,security", required = true)
      private String clientId;
  
      @Metadata(description = "OAuth Consumer Secret of the connected app configured in the Salesforce instance setup.",
                label = "common,security", secret = true)
      private String clientSecret;
  
      @Metadata(description = "Refresh token already obtained in the refresh token OAuth flow. One needs to setup a web"
                              + " application and configure a callback URL to receive the refresh token, or configure using the builtin"
                              + " callback at https://login.salesforce.com/services/oauth2/success or "
                              + " https://test.salesforce.com/services/oauth2/success and then retrive the refresh_token from the URL at the"
                              + " end of the flow. Note that in development organizations Salesforce allows hosting the callback web "
                              + " application at localhost.",
                label = "common,security", secret = true)
      private String refreshToken;
  
      @Metadata(description = "Username used in OAuth flow to gain access to access token. It's easy to get started with"
                              + " password OAuth flow, but in general one should avoid it as it is deemed less secure than other flows.",
                label = "common,security")
      private String userName;
  
      @Metadata(description = "Password used in OAuth flow to gain access to access token. It's easy to get started with"
                              + " password OAuth flow, but in general one should avoid it as it is deemed less secure than other flows."
                              + " Make sure that you append security token to the end of the password if using one.",
                label = "common,security", secret = true)
      private String password;
  
      @Metadata(description = "KeyStore parameters to use in OAuth JWT flow. The KeyStore should contain only one entry"
                              + " with private key and certificate. Salesforce does not verify the certificate chain, so this can easily be"
                              + " a selfsigned certificate. Make sure that you upload the certificate to the corresponding connected app.",
                label = "common,security")
      private KeyStoreParameters keystore;
  
      @Metadata(description = "Value to use for the Audience claim (aud) when using OAuth JWT flow. If not set, the login URL will be used, which is"
                              + " appropriate in most cases.",
                label = "common,security")
      private String jwtAudience;
  
      @Metadata(description = "Explicit authentication method to be used, one of USERNAME_PASSWORD, REFRESH_TOKEN, CLIENT_CREDENTIALS, or JWT."
                              + " Salesforce component can auto-determine the authentication method to use from the properties set, set this "
                              + " property to eliminate any ambiguity.",
                label = "common,security", enums = "USERNAME_PASSWORD,REFRESH_TOKEN,CLIENT_CREDENTIALS,JWT")
      private AuthenticationType authenticationType;
  
      @Metadata(description = "If set to true prevents the component from authenticating to Salesforce with the start of"
                              + " the component. You would generally set this to the (default) false and authenticate early and be immediately"
                              + " aware of any authentication issues. Lazy login is not supported by salesforce consumers.",
                defaultValue = "false", label = "common,security")
      private boolean lazyLogin;
  
      @Metadata(description = "Pub/Sub host",
                defaultValue = "api.pubsub.salesforce.com", label = "common,security")
      private String pubSubHost = "api.pubsub.salesforce.com";
  
      @Metadata(description = "Pub/Sub port",
                defaultValue = "7443", label = "common,security")
      private int pubSubPort = 7443;
  
      @Metadata(description = "Allow the Pub/Sub API client to use the proxy detected by java.net.ProxySelector. If false then"
                              + " no proxy server will be used.",
                defaultValue = "true", label = "common,proxy")
      private boolean pubsubAllowUseSystemProxy = true;
  
      @Metadata(description = "Global endpoint configuration - use to set values that are common to all endpoints",
                label = "common,advanced")
      private SalesforceEndpointConfig config;
  
      @Metadata(description = "Timeout used by the HttpClient when waiting for response from the Salesforce server.",
                label = "common", defaultValue = "" + IDLE_TIMEOUT)
      private long httpClientIdleTimeout = IDLE_TIMEOUT;
  
      @Metadata(description = "Connection timeout used by the HttpClient when connecting to the Salesforce server.",
                label = "common", defaultValue = "" + CONNECTION_TIMEOUT)
      private long httpClientConnectionTimeout = CONNECTION_TIMEOUT;
  
      @Metadata(description = "Timeout value for HTTP requests.",
                label = "common", defaultValue = "" + REQUEST_TIMEOUT)
      private long httpRequestTimeout = REQUEST_TIMEOUT;
  
      @Metadata(description = "Max content length of an HTTP response.", label = "common")
      private Integer httpMaxContentLength;
  
      @Metadata(description = "HTTP request buffer size. May need to be increased for large SOQL" +
                              " queries.",
                label = "common", defaultValue = "" + REQUEST_BUFFER_SIZE)
      private Integer httpRequestBufferSize;
  
      @Metadata(description = "Used to set any properties that can be configured on the underlying HTTP client. Have a"
                              + " look at properties of SalesforceHttpClient and the Jetty HttpClient for all available options.",
                label = "common,advanced")
      private Map<String, Object> httpClientProperties;
  
      @Metadata(description = "Size of the thread pool used to handle HTTP responses.",
                label = "common,advanced", defaultValue = "10")
      private int workerPoolSize = 10;
      @Metadata(description = "Maximum size of the thread pool used to handle HTTP responses.",
                label = "common,advanced", defaultValue = "20")
      private int workerPoolMaxSize = 20;
  
      @Metadata(description = "Used to set any properties that can be configured on the LongPollingTransport used by the"
                              + " BayeuxClient (CometD) used by the streaming api",
                label = "common,advanced")
      private Map<String, Object> longPollingTransportProperties;
  
      @Metadata(description = "SSL parameters to use, see SSLContextParameters class for all available options.",
                label = "common,security")
      private SSLContextParameters sslContextParameters;
      @Metadata(description = "Enable usage of global SSL context parameters", label = "security", defaultValue = "false")
      private boolean useGlobalSslContextParameters;
  
      // Proxy host and port
      @Metadata(description = "Hostname of the HTTP proxy server to use.", label = "common,proxy")
      private String httpProxyHost;
  
      @Metadata(description = "Port number of the HTTP proxy server to use.", label = "common,proxy")
      private Integer httpProxyPort;
  
      @Metadata(description = "If set to true the configures the HTTP proxy to use as a SOCKS4 proxy.", defaultValue = "false",
                label = "common,proxy")
      private boolean httpProxySocks4;
  
      @Metadata(description = "If set to false disables the use of TLS when accessing the HTTP proxy.", defaultValue = "true",
                label = "common,proxy,security")
      private boolean httpProxySecure = true;
  
      @Metadata(description = "A list of addresses for which HTTP proxy server should be used.", label = "common,proxy")
      private Set<String> httpProxyIncludedAddresses;
  
      @Metadata(description = "A list of addresses for which HTTP proxy server should not be used.", label = "common,proxy")
      private Set<String> httpProxyExcludedAddresses;
  
      // Proxy basic authentication
      @Metadata(description = "Username to use to authenticate against the HTTP proxy server.", label = "common,proxy,security")
      private String httpProxyUsername;
  
      @Metadata(description = "Password to use to authenticate against the HTTP proxy server.", label = "common,proxy,security",
                secret = true)
      private String httpProxyPassword;
  
      @Metadata(description = "Used in authentication against the HTTP proxy server, needs to match the URI of the proxy"
                              + " server in order for the httpProxyUsername and httpProxyPassword to be used for authentication.",
                label = "common,proxy,security")
      private String httpProxyAuthUri;
  
      @Metadata(description = "Realm of the proxy server, used in preemptive Basic/Digest authentication methods against"
                              + " the HTTP proxy server.",
                label = "common,proxy,security")
      private String httpProxyRealm;
  
      @Metadata(description = "If set to true Digest authentication will be used when authenticating to the HTTP proxy,"
                              + " otherwise Basic authorization method will be used",
                defaultValue = "false", label = "common,proxy,security")
      private boolean httpProxyUseDigestAuth;
  
      @Metadata(description = "In what packages are the generated DTO classes. Typically the classes would be generated"
                              + " using camel-salesforce-maven-plugin. "
                              + " Set it if using the generated DTOs to gain the benefit of using short "
                              + " SObject names in parameters/header values. Multiple packages can be separated by comma.",
                javaType = "java.lang.String", label = "common")
      private String packages;
  
      @Deprecated
      @Metadata(description = "Timeout in seconds to validate when a custom pubSubReplayId has been configured, when starting the Camel Salesforce consumer.",
                defaultValue = "30", label = "consumer,advanced")
      private int initialReplyIdTimeout = 30;
  
      // component state
      private SalesforceHttpClient httpClient;
  
      private SalesforceSession session;
  
      private Map<String, Class<?>> classMap;
      private Map<String, Class<?>> eventClassMap;
  
      // Lazily created helper for consumer endpoints
      private SubscriptionHelper subscriptionHelper;
  
      public SalesforceComponent() {
          this(null);
      }
  
      public SalesforceComponent(CamelContext context) {
          super(context);
      }
  
      @Override
      protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
          // get Operation from remaining URI
          OperationName operationName = null;
          String topicName = null;
          String apexUrl = null;
          LOG.debug("Creating endpoint for: {}", remaining);
          if (remaining.startsWith(APEX_CALL_PREFIX)) {
              // extract APEX URL
              apexUrl = remaining.substring(APEX_CALL_PREFIX.length());
              remaining = OperationName.APEX_CALL.value();
          } else if (remaining.startsWith(OperationName.SUBSCRIBE.value()) || remaining.startsWith("pubSub")) {
              final String[] parts = remaining.split(":");
              if (parts.length != 2) {
                  throw new IllegalArgumentException("topicName must be supplied for subscribe/pubsub operations.");
              }
              remaining = parts[0];
              topicName = parts[1];
          }
          operationName = OperationName.fromValue(remaining);
  
          // create endpoint config
          if (config == null) {
              config = new SalesforceEndpointConfig();
          }
          if (config.getHttpClient() == null) {
              // set the component's httpClient as default
              config.setHttpClient(httpClient);
          }
  
          // create a deep copy and map parameters
          final SalesforceEndpointConfig copy = config.copy();
          setProperties(copy, parameters);
  
          // set apexUrl in endpoint config
          if (apexUrl != null) {
              copy.setApexUrl(apexUrl);
          }
  
          final SalesforceEndpoint endpoint = new SalesforceEndpoint(uri, this, copy, operationName, topicName);
  
          // map remaining parameters to endpoint (specifically, synchronous)
          setProperties(endpoint, parameters);
  
          // if operation is APEX call, map remaining parameters to query params
          if (operationName == OperationName.APEX_CALL && !parameters.isEmpty()) {
              Map<String, Object> queryParams = new HashMap<>(copy.getApexQueryParams());
  
              // override component params with endpoint params
              queryParams.putAll(parameters);
              parameters.clear();
  
              copy.setApexQueryParams(queryParams);
          }
  
          return endpoint;
      }
  
      private Map<String, Class<?>> parsePackages() {
          Map<String, Class<?>> result = new HashMap<>();
          Set<Class<?>> classes = PluginHelper.getPackageScanClassResolver(getCamelContext())
                  .findImplementations(AbstractDTOBase.class, getPackagesAsArray());
          for (Class<?> aClass : classes) {
              result.put(aClass.getSimpleName(), aClass);
          }
          return result;
      }
  
      private Map<String, Class<?>> scanEventClasses() {
          Map<String, Class<?>> result = new HashMap<>();
  
          Set<Class<?>> classes = PluginHelper.getPackageScanClassResolver(getCamelContext())
                  .findImplementations(SpecificRecord.class, SALESFORCE_EVENTBUS_PACKAGE);
          for (Class<?> aClass : classes) {
              result.put(aClass.getName(), aClass);
          }
          return result;
      }
  
      public SalesforceHttpClient getHttpClient() {
          return httpClient;
      }
  
      @Override
      protected void doBuild() throws Exception {
          super.doBuild();
  
      }
  
      @Override
      protected void doStart() throws Exception {
          super.doStart();
  
          if (loginConfig == null) {
              loginConfig = new SalesforceLoginConfig();
              loginConfig.setInstanceUrl(instanceUrl);
              loginConfig.setClientId(clientId);
              loginConfig.setClientSecret(clientSecret);
              loginConfig.setKeystore(keystore);
              loginConfig.setJwtAudience(jwtAudience);
              loginConfig.setLazyLogin(lazyLogin);
              loginConfig.setLoginUrl(loginUrl);
              loginConfig.setPassword(password);
              loginConfig.setRefreshToken(refreshToken);
              loginConfig.setType(authenticationType);
              loginConfig.setUserName(userName);
  
              LOG.debug("Created login configuration: {}", loginConfig);
          } else {
              LOG.debug("Using shared login configuration: {}", loginConfig);
          }
  
          // create a Jetty HttpClient if not already set
          if (httpClient == null) {
              if (config != null && config.getHttpClient() != null) {
                  httpClient = config.getHttpClient();
              } else {
                  final SSLContextParameters contextParameters = Optional.ofNullable(sslContextParameters)
                          .orElseGet(() -> Optional.ofNullable(retrieveGlobalSslContextParameters())
                                  .orElseGet(() -> new SSLContextParameters()));
  
                  final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
                  sslContextFactory.setSslContext(contextParameters.createSSLContext(getCamelContext()));
  
                  httpClient = createHttpClient(this, sslContextFactory, getCamelContext(), workerPoolSize, workerPoolMaxSize);
              }
              if (config != null) {
                  config.setHttpClient(httpClient);
              }
          }
  
          if (httpClientProperties == null) {
              httpClientProperties = new HashMap<>();
          }
  
          defineComponentPropertiesIn(httpClientProperties, this);
          setupHttpClient(httpClient, getCamelContext(), httpClientProperties);
  
          // support restarts
          if (session == null) {
              session = new SalesforceSession(getCamelContext(), httpClient, httpClient.getTimeout(), loginConfig);
          }
          // set session before calling start()
          httpClient.setSession(session);
  
          // start the Jetty client to initialize thread pool, etc.
          httpClient.start();
  
          // login at startup if lazyLogin is disabled
          if (!loginConfig.isLazyLogin()) {
              ServiceHelper.startService(session);
          }
  
          if (packages != null) {
              // parse the packages to create SObject name to class map
              classMap = parsePackages();
              LOG.info("Found {} generated classes in packages: {}", classMap.size(), packages);
          } else {
              // use an empty map to avoid NPEs later
              LOG.warn("Missing property packages, getSObject* operations will NOT work without property rawPayload=true");
              classMap = new HashMap<>(0);
          }
  
          this.eventClassMap = scanEventClasses();
  
          if (subscriptionHelper != null) {
              ServiceHelper.startService(subscriptionHelper);
          }
      }
  
      @Override
      protected void doStop() throws Exception {
          if (classMap != null) {
              classMap.clear();
          }
          if (eventClassMap != null) {
              eventClassMap.clear();
          }
  
          try {
              if (subscriptionHelper != null) {
                  // shutdown all streaming connections
                  // note that this is done in the component, and not in consumer
                  ServiceHelper.stopService(subscriptionHelper);
                  subscriptionHelper = null;
              }
              if (session != null && session.getAccessToken() != null) {
                  // logout of Salesforce
                  ServiceHelper.stopService(session);
              }
          } finally {
              if (httpClient != null) {
                  // shutdown http client connections
                  httpClient.stop();
                  // destroy http client if it was created by the component
                  if (config != null && config.getHttpClient() == null) {
                      httpClient.destroy();
                  }
                  httpClient = null;
              }
          }
      }
  
      public SubscriptionHelper getSubscriptionHelper() throws Exception {
          if (subscriptionHelper == null) {
              // lazily create subscription helper
              subscriptionHelper = new SubscriptionHelper(this);
          }
          return subscriptionHelper;
      }
  
      public AuthenticationType getAuthenticationType() {
          return authenticationType;
      }
  
      public void setAuthenticationType(AuthenticationType authenticationType) {
          this.authenticationType = authenticationType;
      }
  
      public SalesforceLoginConfig getLoginConfig() {
          return loginConfig;
      }
  
      public void setLoginConfig(SalesforceLoginConfig loginConfig) {
          this.loginConfig = loginConfig;
      }
  
      public void setInstanceUrl(String instanceUrl) {
          this.instanceUrl = instanceUrl;
      }
  
      public String getInstanceUrl() {
          return instanceUrl;
      }
  
      public String getLoginUrl() {
          return loginUrl;
      }
  
      public void setLoginUrl(String loginUrl) {
          this.loginUrl = loginUrl;
      }
  
      public String getClientId() {
          return clientId;
      }
  
      public void setClientId(String clientId) {
          this.clientId = clientId;
      }
  
      public String getClientSecret() {
          return clientSecret;
  
      }
  
      public void setClientSecret(String clientSecret) {
          this.clientSecret = clientSecret;
      }
  
      public void setKeystore(final KeyStoreParameters keystore) {
          this.keystore = keystore;
      }
  
      public KeyStoreParameters getKeystore() {
          return keystore;
      }
  
      public String getJwtAudience() {
          return jwtAudience;
      }
  
      public void setJwtAudience(String jwtAudience) {
          this.jwtAudience = jwtAudience;
      }
  
      public String getRefreshToken() {
          return refreshToken;
      }
  
      public void setRefreshToken(String refreshToken) {
          this.refreshToken = refreshToken;
      }
  
      public String getUserName() {
          return userName;
      }
  
      public void setUserName(String userName) {
          this.userName = userName;
      }
  
      public String getPassword() {
          return password;
      }
  
      public void setPassword(String password) {
          this.password = password;
      }
  
      public boolean isLazyLogin() {
          return lazyLogin;
      }
  
      public void setLazyLogin(boolean lazyLogin) {
          this.lazyLogin = lazyLogin;
      }
  
      public String getPubSubHost() {
          return pubSubHost;
      }
  
      public void setPubSubHost(String pubSubHost) {
          this.pubSubHost = pubSubHost;
      }
  
      public int getPubSubPort() {
          return pubSubPort;
      }
  
      public void setPubSubPort(int pubSubPort) {
          this.pubSubPort = pubSubPort;
      }
  
      public SalesforceEndpointConfig getConfig() {
          return config;
      }
  
      public void setConfig(SalesforceEndpointConfig config) {
          this.config = config;
      }
  
      public Map<String, Object> getHttpClientProperties() {
          return httpClientProperties;
      }
  
      public void setHttpClientProperties(Map<String, Object> httpClientProperties) {
          this.httpClientProperties = httpClientProperties;
      }
  
      public Map<String, Object> getLongPollingTransportProperties() {
          return longPollingTransportProperties;
      }
  
      public void setLongPollingTransportProperties(Map<String, Object> longPollingTransportProperties) {
          this.longPollingTransportProperties = longPollingTransportProperties;
      }
  
      public SSLContextParameters getSslContextParameters() {
          return sslContextParameters;
      }
  
      public void setSslContextParameters(SSLContextParameters sslContextParameters) {
          this.sslContextParameters = sslContextParameters;
      }
  
      @Override
      public boolean isUseGlobalSslContextParameters() {
          return this.useGlobalSslContextParameters;
      }
  
      @Override
      public void setUseGlobalSslContextParameters(boolean useGlobalSslContextParameters) {
          this.useGlobalSslContextParameters = useGlobalSslContextParameters;
      }
  
      public long getHttpClientIdleTimeout() {
          return httpClientIdleTimeout;
      }
  
      public void setHttpClientIdleTimeout(long httpClientIdleTimeout) {
          this.httpClientIdleTimeout = httpClientIdleTimeout;
      }
  
      public long getHttpClientConnectionTimeout() {
          return httpClientConnectionTimeout;
      }
  
      public void setHttpClientConnectionTimeout(long httpClientConnectionTimeout) {
          this.httpClientConnectionTimeout = httpClientConnectionTimeout;
      }
  
      public long getHttpRequestTimeout() {
          return httpRequestTimeout;
      }
  
      public void setHttpRequestTimeout(long httpRequestTimeout) {
          this.httpRequestTimeout = httpRequestTimeout;
      }
  
      public Integer getHttpMaxContentLength() {
          return httpMaxContentLength;
      }
  
      public void setHttpMaxContentLength(Integer httpMaxContentLength) {
          this.httpMaxContentLength = httpMaxContentLength;
      }
  
      public Integer getHttpRequestBufferSize() {
          return httpRequestBufferSize;
      }
  
      public void setHttpRequestBufferSize(Integer httpRequestBufferSize) {
          this.httpRequestBufferSize = httpRequestBufferSize;
      }
  
      public String getHttpProxyHost() {
          return httpProxyHost;
      }
  
      public void setHttpProxyHost(String httpProxyHost) {
          this.httpProxyHost = httpProxyHost;
      }
  
      public Integer getHttpProxyPort() {
          return httpProxyPort;
      }
  
      public void setHttpProxyPort(Integer httpProxyPort) {
          this.httpProxyPort = httpProxyPort;
      }
  
      public String getHttpProxyUsername() {
          return httpProxyUsername;
      }
  
      public void setHttpProxyUsername(String httpProxyUsername) {
          this.httpProxyUsername = httpProxyUsername;
      }
  
      public String getHttpProxyPassword() {
          return httpProxyPassword;
      }
  
      public void setHttpProxyPassword(String httpProxyPassword) {
          this.httpProxyPassword = httpProxyPassword;
      }
  
      public boolean isHttpProxySocks4() {
          return httpProxySocks4;
      }
  
      public void setHttpProxySocks4(boolean isHttpProxySocks4) {
          this.httpProxySocks4 = isHttpProxySocks4;
      }
  
      public boolean isHttpProxySecure() {
          return httpProxySecure;
      }
  
      public void setHttpProxySecure(boolean isHttpProxySecure) {
          this.httpProxySecure = isHttpProxySecure;
      }
  
      public Set<String> getHttpProxyIncludedAddresses() {
          return httpProxyIncludedAddresses;
      }
  
      public void setHttpProxyIncludedAddresses(Set<String> httpProxyIncludedAddresses) {
          this.httpProxyIncludedAddresses = httpProxyIncludedAddresses;
      }
  
      public Set<String> getHttpProxyExcludedAddresses() {
          return httpProxyExcludedAddresses;
      }
  
      public void setHttpProxyExcludedAddresses(Set<String> httpProxyExcludedAddresses) {
          this.httpProxyExcludedAddresses = httpProxyExcludedAddresses;
      }
  
      public String getHttpProxyAuthUri() {
          return httpProxyAuthUri;
      }
  
      public void setHttpProxyAuthUri(String httpProxyAuthUri) {
          this.httpProxyAuthUri = httpProxyAuthUri;
      }
  
      public String getHttpProxyRealm() {
          return httpProxyRealm;
      }
  
      public void setHttpProxyRealm(String httpProxyRealm) {
          this.httpProxyRealm = httpProxyRealm;
      }
  
      public boolean isHttpProxyUseDigestAuth() {
          return httpProxyUseDigestAuth;
      }
  
      public void setHttpProxyUseDigestAuth(boolean httpProxyUseDigestAuth) {
          this.httpProxyUseDigestAuth = httpProxyUseDigestAuth;
      }
  
      public boolean isPubsubAllowUseSystemProxy() {
          return pubsubAllowUseSystemProxy;
      }
  
      public void setPubsubAllowUseSystemProxy(boolean pubsubAllowUseSystemProxy) {
          this.pubsubAllowUseSystemProxy = pubsubAllowUseSystemProxy;
      }
  
      public int getWorkerPoolSize() {
          return workerPoolSize;
      }
  
      public void setWorkerPoolSize(int workerPoolSize) {
          this.workerPoolSize = workerPoolSize;
      }
  
      public int getWorkerPoolMaxSize() {
          return workerPoolMaxSize;
      }
  
      public void setWorkerPoolMaxSize(int workerPoolMaxSize) {
          this.workerPoolMaxSize = workerPoolMaxSize;
      }
  
      public String getPackages() {
          return packages;
      }
  
      public void setPackages(String packages) {
          this.packages = packages;
      }
  
      public String[] getPackagesAsArray() {
          if (packages != null) {
              return packages.split(",");
          } else {
              return null;
          }
      }
  
      public int getInitialReplyIdTimeout() {
          return initialReplyIdTimeout;
      }
  
      public void setInitialReplyIdTimeout(int initialReplyIdTimeout) {
          this.initialReplyIdTimeout = initialReplyIdTimeout;
      }
  
      public SalesforceSession getSession() {
          return session;
      }
  
      public Map<String, Class<?>> getClassMap() {
          return classMap;
      }
  
      public Map<String, Class<?>> getEventClassMap() {
          return eventClassMap;
      }
  
      public RestClient createRestClientFor(final SalesforceEndpoint endpoint) throws SalesforceException {
          final SalesforceEndpointConfig endpointConfig = endpoint.getConfiguration();
  
          return createRestClientFor(endpointConfig);
      }
  
      RestClient createRestClientFor(SalesforceEndpointConfig endpointConfig) throws SalesforceException {
          final String version = endpointConfig.getApiVersion();
  
          return new DefaultRestClient(httpClient, version, session, loginConfig);
      }
  
      RestClient createRestClient(final Map<String, Object> properties) throws Exception {
          final SalesforceEndpointConfig modifiedConfig = Optional.ofNullable(config).map(SalesforceEndpointConfig::copy)
                  .orElseGet(() -> new SalesforceEndpointConfig());
          final CamelContext camelContext = getCamelContext();
  
          PropertyBindingSupport.bindProperties(camelContext, modifiedConfig, properties);
  
          return createRestClientFor(modifiedConfig);
      }
  
      static RestClient createRestClient(final CamelContext camelContext, final Map<String, Object> properties) throws Exception {
  
          final SalesforceEndpointConfig config = new SalesforceEndpointConfig();
          // let's work with a copy so original properties are intact
          PropertyBindingSupport.bindProperties(camelContext, config, new HashMap<>(properties));
  
          final SalesforceLoginConfig loginConfig = new SalesforceLoginConfig();
          // let's work with a copy so original properties are intact
          PropertyBindingSupport.bindProperties(camelContext, loginConfig, new HashMap<>(properties));
  
          final SSLContextParameters sslContextParameters
                  = Optional.ofNullable(camelContext.getSSLContextParameters()).orElseGet(() -> new SSLContextParameters());
          // let's work with a copy so original properties are intact
          PropertyBindingSupport.bindProperties(camelContext, sslContextParameters, new HashMap<>(properties));
  
          final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
          sslContextFactory.setSslContext(sslContextParameters.createSSLContext(camelContext));
  
          final SalesforceHttpClient httpClient
                  = createHttpClient("SalesforceComponent", sslContextFactory, camelContext, 10, 20);
          setupHttpClient(httpClient, camelContext, properties);
  
          final SalesforceSession session = new SalesforceSession(camelContext, httpClient, httpClient.getTimeout(), loginConfig);
          httpClient.setSession(session);
  
          return new DefaultRestClient(httpClient, config.getApiVersion(), session, loginConfig);
      }
  
      public RawClient createRawClientFor(SalesforceEndpoint endpoint) throws SalesforceException {
          return new DefaultRawClient(httpClient, "", session, loginConfig);
      }
  
      static SalesforceHttpClient createHttpClient(
              Object source, final SslContextFactory.Client sslContextFactory, final CamelContext context, int workerPoolSize,
              int workerPoolMaxSize) {
          SecurityUtils.adaptToIBMCipherNames(sslContextFactory);
  
          final SalesforceHttpClient httpClient = new SalesforceHttpClient(
                  context, context.getExecutorServiceManager().newThreadPool(source, "SalesforceHttpClient", workerPoolSize,
                          workerPoolMaxSize),
                  sslContextFactory);
          // default settings, use httpClientProperties to set other
          // properties
          httpClient.setConnectTimeout(CONNECTION_TIMEOUT);
          httpClient.setIdleTimeout(IDLE_TIMEOUT);
  
          return httpClient;
      }
  
      static SalesforceHttpClient setupHttpClient(
              final SalesforceHttpClient httpClient, final CamelContext camelContext,
              final Map<String, Object> httpClientProperties)
              throws Exception {
  
          if (httpClientProperties == null || httpClientProperties.isEmpty()) {
              return httpClient;
          }
  
          // set HTTP client parameters
          final TypeConverter typeConverter = camelContext.getTypeConverter();
          PropertyBindingSupport.bindProperties(camelContext, httpClient, new HashMap<>(httpClientProperties));
  
          final Long httpConnectionTimeout
                  = typeConverter.convertTo(Long.class, httpClientProperties.get(HTTP_CONNECTION_TIMEOUT));
          final Long httpIdleTimeout = typeConverter.convertTo(Long.class, httpClientProperties.get(HTTP_IDLE_TIMEOUT));
          final Long httpRequestTimeout = typeConverter.convertTo(Long.class, httpClientProperties.get(HTTP_REQUEST_TIMEOUT));
          final Integer maxContentLength
                  = typeConverter.convertTo(Integer.class, httpClientProperties.get(HTTP_MAX_CONTENT_LENGTH));
          Integer requestBufferSize
                  = typeConverter.convertTo(Integer.class, httpClientProperties.get(HTTP_REQUEST_BUFFER_SIZE));
          if (requestBufferSize == null) {
              requestBufferSize = REQUEST_BUFFER_SIZE;
          }
  
          final String httpProxyHost = typeConverter.convertTo(String.class, httpClientProperties.get(HTTP_PROXY_HOST));
          final Integer httpProxyPort = typeConverter.convertTo(Integer.class, httpClientProperties.get(HTTP_PROXY_PORT));
          final boolean isHttpProxySocks4
                  = typeConverter.convertTo(boolean.class, httpClientProperties.get(HTTP_PROXY_IS_SOCKS4));
          final boolean isHttpProxySecure
                  = typeConverter.convertTo(boolean.class, httpClientProperties.get(HTTP_PROXY_IS_SECURE));
          @SuppressWarnings("unchecked")
          final Set<String> httpProxyIncludedAddresses = (Set<String>) httpClientProperties.get(HTTP_PROXY_INCLUDE);
          @SuppressWarnings("unchecked")
          final Set<String> httpProxyExcludedAddresses = (Set<String>) httpClientProperties.get(HTTP_PROXY_EXCLUDE);
          final String httpProxyUsername = typeConverter.convertTo(String.class, httpClientProperties.get(HTTP_PROXY_USERNAME));
          final String httpProxyPassword = typeConverter.convertTo(String.class, httpClientProperties.get(HTTP_PROXY_PASSWORD));
          final String httpProxyAuthUri = typeConverter.convertTo(String.class, httpClientProperties.get(HTTP_PROXY_AUTH_URI));
          final String httpProxyRealm = typeConverter.convertTo(String.class, httpClientProperties.get(HTTP_PROXY_REALM));
          final boolean httpProxyUseDigestAuth
                  = typeConverter.convertTo(boolean.class, httpClientProperties.get(HTTP_PROXY_USE_DIGEST_AUTH));
  
          // set HTTP timeout settings
          if (httpIdleTimeout != null) {
              httpClient.setIdleTimeout(httpIdleTimeout);
          }
          if (httpConnectionTimeout != null) {
              httpClient.setConnectTimeout(httpConnectionTimeout);
          }
          if (maxContentLength != null) {
              httpClient.setMaxContentLength(maxContentLength);
          }
          if (httpRequestTimeout != null) {
              httpClient.setTimeout(httpRequestTimeout);
          }
          httpClient.setRequestBufferSize(requestBufferSize);
  
          // set HTTP proxy settings
          if (httpProxyHost != null && httpProxyPort != null) {
              Origin.Address proxyAddress = new Origin.Address(httpProxyHost, httpProxyPort);
              ProxyConfiguration.Proxy proxy;
              if (isHttpProxySocks4) {
                  proxy = new Socks4Proxy(proxyAddress, isHttpProxySecure);
              } else {
                  proxy = new HttpProxy(proxyAddress, isHttpProxySecure);
              }
              if (httpProxyIncludedAddresses != null && !httpProxyIncludedAddresses.isEmpty()) {
                  proxy.getIncludedAddresses().addAll(httpProxyIncludedAddresses);
              }
              if (httpProxyExcludedAddresses != null && !httpProxyExcludedAddresses.isEmpty()) {
                  proxy.getExcludedAddresses().addAll(httpProxyExcludedAddresses);
              }
              httpClient.getProxyConfiguration().addProxy(proxy);
          }
          if (httpProxyUsername != null && httpProxyPassword != null) {
              StringHelper.notEmpty(httpProxyAuthUri, "httpProxyAuthUri");
              StringHelper.notEmpty(httpProxyRealm, "httpProxyRealm");
  
              final Authentication authentication;
              if (httpProxyUseDigestAuth) {
                  authentication = new DigestAuthentication(
                          new URI(httpProxyAuthUri), httpProxyRealm, httpProxyUsername, httpProxyPassword);
              } else {
                  authentication = new BasicAuthentication(
                          new URI(httpProxyAuthUri), httpProxyRealm, httpProxyUsername, httpProxyPassword);
              }
              httpClient.getAuthenticationStore().addAuthentication(authentication);
          }
          return httpClient;
      }
  
      private static void defineComponentPropertiesIn(
              final Map<String, Object> httpClientProperties, final SalesforceComponent salesforce) {
          putValueIfGivenTo(httpClientProperties, HTTP_IDLE_TIMEOUT, salesforce::getHttpClientIdleTimeout);
          putValueIfGivenTo(httpClientProperties, HTTP_CONNECTION_TIMEOUT, salesforce::getHttpClientConnectionTimeout);
          putValueIfGivenTo(httpClientProperties, HTTP_REQUEST_TIMEOUT, salesforce::getHttpRequestTimeout);
  
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_HOST, salesforce::getHttpProxyHost);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_PORT, salesforce::getHttpProxyPort);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_INCLUDE, salesforce::getHttpProxyIncludedAddresses);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_EXCLUDE, salesforce::getHttpProxyExcludedAddresses);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_USERNAME, salesforce::getHttpProxyUsername);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_PASSWORD, salesforce::getHttpProxyPassword);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_REALM, salesforce::getHttpProxyRealm);
          putValueIfGivenTo(httpClientProperties, HTTP_PROXY_AUTH_URI, salesforce::getHttpProxyAuthUri);
          putValueIfGivenTo(httpClientProperties, HTTP_MAX_CONTENT_LENGTH, salesforce::getHttpMaxContentLength);
          putValueIfGivenTo(httpClientProperties, HTTP_REQUEST_BUFFER_SIZE, salesforce::getHttpRequestBufferSize);
  
          if (ObjectHelper.isNotEmpty(salesforce.getHttpProxyHost())) {
              // let's not put `false` values in client properties if no proxy is
              // used
              putValueIfGivenTo(httpClientProperties, HTTP_PROXY_IS_SOCKS4, salesforce::isHttpProxySocks4);
              putValueIfGivenTo(httpClientProperties, HTTP_PROXY_IS_SECURE, salesforce::isHttpProxySecure);
              putValueIfGivenTo(httpClientProperties, HTTP_PROXY_USE_DIGEST_AUTH, salesforce::isHttpProxyUseDigestAuth);
          }
      }
  
      private static void putValueIfGivenTo(
              final Map<String, Object> properties, final String key, final Supplier<Object> valueSupplier) {
          final Object value = valueSupplier.get();
          if (ObjectHelper.isNotEmpty(value)) {
              properties.putIfAbsent(key, value);
          }
      }
  }
  