CloudhopperBuilder.java

package fr.sii.ogham.sms.builder.cloudhopper;


import static fr.sii.ogham.sms.CloudhopperConstants.DEFAULT_BIND_TYPE;
import static fr.sii.ogham.sms.CloudhopperConstants.DEFAULT_CHARSET;
import static fr.sii.ogham.sms.CloudhopperConstants.DEFAULT_INTERFACE_VERSION;
import static fr.sii.ogham.sms.CloudhopperConstants.DEFAULT_RESPONSE_TIMEOUT;
import static fr.sii.ogham.sms.CloudhopperConstants.DEFAULT_UNBIND_TIMEOUT;

import java.util.function.Consumer;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

import com.cloudhopper.commons.charset.Charset;
import com.cloudhopper.smpp.SmppBindType;
import com.cloudhopper.smpp.SmppClient;
import com.cloudhopper.smpp.SmppConstants;
import com.cloudhopper.smpp.SmppSessionConfiguration;
import com.cloudhopper.smpp.SmppSessionHandler;
import com.cloudhopper.smpp.impl.DefaultSmppClient;
import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
import com.cloudhopper.smpp.pdu.Pdu;
import com.cloudhopper.smpp.pdu.SubmitSm;
import com.cloudhopper.smpp.ssl.SslConfiguration;
import com.cloudhopper.smpp.type.Address;
import com.cloudhopper.smpp.type.LoggingOptions;

import fr.sii.ogham.core.async.ThreadSleepAwaiter;
import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilder;
import fr.sii.ogham.core.builder.configuration.ConfigurationValueBuilderHelper;
import fr.sii.ogham.core.builder.configurer.Configurer;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.context.DefaultBuildContext;
import fr.sii.ogham.core.fluent.AbstractParent;
import fr.sii.ogham.core.retry.RetryExecutor;
import fr.sii.ogham.core.retry.SimpleRetryExecutor;
import fr.sii.ogham.sms.builder.SmsBuilder;
import fr.sii.ogham.sms.builder.cloudhopper.UserDataBuilder.UserDataPropValues;
import fr.sii.ogham.sms.encoder.Encoder;
import fr.sii.ogham.sms.message.Sms;
import fr.sii.ogham.sms.message.addressing.translator.CompositePhoneNumberTranslator;
import fr.sii.ogham.sms.message.addressing.translator.DefaultHandler;
import fr.sii.ogham.sms.message.addressing.translator.PhoneNumberTranslator;
import fr.sii.ogham.sms.sender.impl.CloudhopperSMPPSender;
import fr.sii.ogham.sms.sender.impl.cloudhopper.ExtendedSmppSessionConfiguration;
import fr.sii.ogham.sms.sender.impl.cloudhopper.KeepAliveOptions;
import fr.sii.ogham.sms.sender.impl.cloudhopper.encoder.CloudhopperCharsetSupportingEncoder;
import fr.sii.ogham.sms.sender.impl.cloudhopper.encoder.NamedCharset;
import fr.sii.ogham.sms.sender.impl.cloudhopper.preparator.CharsetMapToCharacterEncodingGroupDataCodingProvider;
import fr.sii.ogham.sms.sender.impl.cloudhopper.preparator.DataCodingProvider;
import fr.sii.ogham.sms.sender.impl.cloudhopper.preparator.MessagePreparator;
import fr.sii.ogham.sms.sender.impl.cloudhopper.preparator.ShortMessagePreparator;
import fr.sii.ogham.sms.sender.impl.cloudhopper.preparator.TlvMessagePayloadMessagePreparator;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.AlwaysNewSessionStrategy;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.DefaultErrorAnalyzer;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.ErrorAnalyzer;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.ErrorHandler;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.KeepSessionAliveStrategy;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.LogErrorHandler;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.MayReuseSessionStrategy;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.RespondToDeliveryReceiptHandler;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.RespondToEnquireLinkRequestHandler;
import fr.sii.ogham.sms.sender.impl.cloudhopper.session.SessionHandlingStrategy;
import fr.sii.ogham.sms.splitter.GsmMessageSplitter;
import fr.sii.ogham.sms.splitter.MessageSplitter;
import fr.sii.ogham.sms.splitter.NoSplitMessageSplitter;
import fr.sii.ogham.sms.splitter.ReferenceNumberGenerator;

/**
 * Configures Cloudhopper:
 * 
 * <ul>
 * <li>SMPP protocol parameters (host, port, systemId, password,
 * version...)</li>
 * <li>Session management (name, bind, timeouts, retry...)</li>
 * <li>SSL configuration</li>
 * <li>Logging options</li>
 * </ul>
 * 
 * <p>
 * To send {@link Sms} using Cloudhopper, you need to register this builder into
 * a {@link MessagingBuilder} like this:
 * 
 * <pre>
 * <code>
 * MessagingBuilder msgBuilder = ...
 * msgBuilder.sms()
 *    .sender(CloudhopperBuilder.class)    // registers the builder and accesses to that builder for configuring it
 * </code>
 * </pre>
 * 
 * Once the builder is registered, sending sms through Cloudhopper requires at
 * least host of the SMPP server. You can define it using:
 * 
 * <pre>
 * <code>
 * msgBuilder.sms()
 *    .sender(CloudhopperBuilder.class)    // registers the builder and accesses to that builder for configuring it
 *       .host("localhost")
 * </code>
 * </pre>
 * 
 * Or you can also use property keys (using interpolation):
 * 
 * <pre>
 * <code>
 * msgBuilder
 * .environment()
 *    .properties()
 *       .set("custom.property.for.host", "localhost")
 *       .and()
 *    .and()
 * .sms()
 *    .sender(CloudhopperBuilder.class)    // registers the builder and accesses to that builder for configuring it
 *       .host()
 *         .properties("${custom.property.for.host}")
 * </code>
 * </pre>
 * 
 * You can do the same with port of the SMPP server.
 * 
 * 
 * <p>
 * SMPP server may require authentication. In most cases, authentication is done
 * using system_id/password. You can use this builder to quickly provide your
 * system_id and password:
 * 
 * <pre>
 * <code>
 * .sender(CloudhopperBuilder.class)
 *        .systemId("foo")
 *        .password("bar")
 * </code>
 * </pre>
 * 
 * 
 * @author Aurélien Baudet
 */
public class CloudhopperBuilder extends AbstractParent<SmsBuilder> implements Builder<CloudhopperSMPPSender> {
	private static final Logger LOG = LoggerFactory.getLogger(CloudhopperBuilder.class);

	private final ReadableEncoderBuilder sharedEncoderBuilder;
	private BuildContext buildContext;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, String> systemIdValueBuilder;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, String> passwordValueBuilder;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, String> hostValueBuilder;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, Integer> portValueBuilder;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, String> systemTypeValueBuilder;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, InterfaceVersion> interfaceVersionValueBuilder;
	private final ConfigurationValueBuilderHelper<CloudhopperBuilder, SmppBindType> bindTypeValueBuilder;
	private SessionBuilder sessionBuilder;
	private ExtendedSmppSessionConfiguration sessionConfiguration;
	private Address addressRange;
	private SslBuilder sslBuilder;
	private LoggingBuilder loggingBuilder;
	private SmppClientSupplier clientSupplier;
	private SmppSessionHandlerSupplier smppSessionHandler;
	private MessageSplitterBuilder messageSplitterBuilder;
	private EncoderBuilder encoderBuilder;
	private UserDataBuilder userDataBuilder;
	private DataCodingSchemeBuilder dataCodingBuilder;
	private MessagePreparator preparator;

	/**
	 * Default constructor when using without all Ogham work.
	 * 
	 * <strong>WARNING: use is only if you know what you are doing !</strong>
	 */
	public CloudhopperBuilder() {
		this(null, new DefaultBuildContext());
	}

	/**
	 * Constructor that is called when using Ogham builder:
	 * 
	 * <pre>
	 * MessagingBuilder msgBuilder = ...
	 * msgBuilder
	 * .sms()
	 *    .sender(CloudhopperBuilder.class)
	 * </pre>
	 * 
	 * @param parent
	 *            the parent builder instance for fluent chaining
	 * @param buildContext
	 *            for registering instances and property evaluation
	 */
	public CloudhopperBuilder(SmsBuilder parent, BuildContext buildContext) {
		super(parent);
		this.buildContext = buildContext;
		sharedEncoderBuilder = new ReadableEncoderBuilder(buildContext);
		systemIdValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
		passwordValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
		hostValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
		portValueBuilder = buildContext.newConfigurationValueBuilder(this, Integer.class);
		interfaceVersionValueBuilder = buildContext.newConfigurationValueBuilder(this, InterfaceVersion.class);
		systemTypeValueBuilder = buildContext.newConfigurationValueBuilder(this, String.class);
		bindTypeValueBuilder = buildContext.newConfigurationValueBuilder(this, SmppBindType.class);
	}

	/**
	 * The system_id parameter is used to identify an ESME ( External Short
	 * Message Entity) or an SMSC (Short Message Service Centre) at bind time.
	 * An ESME system_id identifies the ESME or ESME agent to the SMSC. The SMSC
	 * system_id provides an identification of the SMSC to the ESME.
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #systemId()}.
	 * 
	 * <pre>
	 * .systemId("my-system-id")
	 * .systemId()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-system-id")
	 * </pre>
	 * 
	 * <pre>
	 * .systemId("my-system-id")
	 * .systemId()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-system-id")
	 * </pre>
	 * 
	 * In both cases, {@code systemId("my-system-id")} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param systemId
	 *            the system_id value
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder systemId(String systemId) {
		systemIdValueBuilder.setValue(systemId);
		return this;
	}

	/**
	 * The system_id parameter is used to identify an ESME ( External Short
	 * Message Entity) or an SMSC (Short Message Service Centre) at bind time.
	 * An ESME system_id identifies the ESME or ESME agent to the SMSC. The SMSC
	 * system_id provides an identification of the SMSC to the ESME.
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .systemId()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-system-id")
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #systemId(String)} takes precedence over
	 * property values and default value.
	 * 
	 * <pre>
	 * .systemId("my-system-id")
	 * .systemId()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-system-id")
	 * </pre>
	 * 
	 * The value {@code "my-system-id"} is used regardless of the value of the
	 * properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, String> systemId() {
		return systemIdValueBuilder;
	}

	/**
	 * The system_type parameter is used to categorize the type of ESME that is
	 * binding to the SMSC. Examples include “VMS” (voice mail system) and “OTA”
	 * (over-the-air activation system). Specification of the system_type is
	 * optional - some SMSC’s may not require ESME’s to provide this detail. In
	 * this case, the ESME can set the system_type to NULL. The system_type
	 * (optional) may be used to categorize the system, e.g., “EMAIL”, “WWW”,
	 * etc.
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #systemType()}.
	 * 
	 * <pre>
	 * .systemType("my-system-type")
	 * .systemType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-system-type")
	 * </pre>
	 * 
	 * <pre>
	 * .systemType("my-system-type")
	 * .systemType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-system-type")
	 * </pre>
	 * 
	 * In both cases, {@code systemType("my-system-type")} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param systemType
	 *            the system type value
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder systemType(String systemType) {
		systemTypeValueBuilder.setValue(systemType);
		return this;
	}

	/**
	 * The system_type parameter is used to categorize the type of ESME that is
	 * binding to the SMSC. Examples include “VMS” (voice mail system) and “OTA”
	 * (over-the-air activation system). Specification of the system_type is
	 * optional - some SMSC’s may not require ESME’s to provide this detail. In
	 * this case, the ESME can set the system_type to NULL. The system_type
	 * (optional) may be used to categorize the system, e.g., “EMAIL”, “WWW”,
	 * etc.
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .systemType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("defaut-system-type")
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #systemType(String)} takes precedence
	 * over property values and default value.
	 * 
	 * <pre>
	 * .systemType("my-system-type")
	 * .systemType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("defaut-system-type")
	 * </pre>
	 * 
	 * The value {@code "my-system-type"} is used regardless of the value of the
	 * properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, String> systemType() {
		return systemTypeValueBuilder;
	}

	/**
	 * The password parameter is used by the SMSC to authenticate the identity
	 * of the binding ESME. The Service Provider may require ESME’s to provide a
	 * password when binding to the SMSC. This password is normally issued by
	 * the SMSC system administrator. The password parameter may also be used by
	 * the ESME to authenticate the identity of the binding SMSC (e.g. in the
	 * case of the outbind operation).
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #password()}.
	 * 
	 * <pre>
	 * .password("my-password")
	 * .password()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-password")
	 * </pre>
	 * 
	 * <pre>
	 * .password("my-password")
	 * .password()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-password")
	 * </pre>
	 * 
	 * In both cases, {@code password("my-password")} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param password
	 *            the password used to authenticate
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder password(String password) {
		passwordValueBuilder.setValue(password);
		return this;
	}

	/**
	 * The password parameter is used by the SMSC to authenticate the identity
	 * of the binding ESME. The Service Provider may require ESME’s to provide a
	 * password when binding to the SMSC. This password is normally issued by
	 * the SMSC system administrator. The password parameter may also be used by
	 * the ESME to authenticate the identity of the binding SMSC (e.g. in the
	 * case of the outbind operation).
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .password()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-password")
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #password(String)} takes precedence over
	 * property values and default value.
	 * 
	 * <pre>
	 * .password("my-password")
	 * .password()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-password")
	 * </pre>
	 * 
	 * The value {@code "my-password"} is used regardless of the value of the
	 * properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, String> password() {
		return passwordValueBuilder;
	}

	/**
	 * The SMPP server host (IP or address).
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #host()}.
	 * 
	 * <pre>
	 * .host("localhost")
	 * .host()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-host")
	 * </pre>
	 * 
	 * <pre>
	 * .host("localhost")
	 * .host()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-host")
	 * </pre>
	 * 
	 * In both cases, {@code host("localhost")} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param host
	 *            the host address
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder host(String host) {
		hostValueBuilder.setValue(host);
		return this;
	}

	/**
	 * The SMPP server host (IP or address).
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .host()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-host")
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #host(String)} takes precedence over
	 * property values and default value.
	 * 
	 * <pre>
	 * .host("localhost")
	 * .host()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue("default-host")
	 * </pre>
	 * 
	 * The value {@code "localhost"} is used regardless of the value of the
	 * properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, String> host() {
		return hostValueBuilder;
	}

	/**
	 * Set the SMPP server port.
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #port()}.
	 * 
	 * <pre>
	 * .port(2775)
	 * .port()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(1775)
	 * </pre>
	 * 
	 * <pre>
	 * .port(2775)
	 * .port()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(1775)
	 * </pre>
	 * 
	 * In both cases, {@code port(2775)} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param port
	 *            the SMPP server port
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder port(Integer port) {
		portValueBuilder.setValue(port);
		return this;
	}

	/**
	 * Set the SMPP server port.
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .port()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(1775)
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #port(Integer)} takes precedence over
	 * property values and default value.
	 * 
	 * <pre>
	 * .port(2775)
	 * .port()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(1775)
	 * </pre>
	 * 
	 * The value {@code 2775} is used regardless of the value of the properties
	 * and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, Integer> port() {
		return portValueBuilder;
	}

	/**
	 * The SMPP protocol version (one of {@link InterfaceVersion#VERSION_3_3},
	 * {@link InterfaceVersion#VERSION_3_4},
	 * {@link InterfaceVersion#VERSION_5_0}).
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #interfaceVersion()}.
	 * 
	 * <pre>
	 * .interfaceVersion(InterfaceVersion.VERSION_5_0)
	 * .interfaceVersion()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(InterfaceVersion.VERSION_3_4)
	 * </pre>
	 * 
	 * <pre>
	 * .interfaceVersion(InterfaceVersion.VERSION_5_0)
	 * .interfaceVersion()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(InterfaceVersion.VERSION_3_4)
	 * </pre>
	 * 
	 * In both cases, {@code interfaceVersion(InterfaceVersion.VERSION_5_0)} is
	 * used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param version
	 *            the version of the SMPP protocol
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder interfaceVersion(InterfaceVersion version) {
		interfaceVersionValueBuilder.setValue(version);
		return this;
	}

	/**
	 * The SMPP protocol version (one of {@link SmppConstants#VERSION_3_3},
	 * {@link SmppConstants#VERSION_3_4}, {@link SmppConstants#VERSION_5_0}).
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #interfaceVersion()}.
	 * 
	 * <pre>
	 * .interfaceVersion(SmppConstants.VERSION_5_0)
	 * .interfaceVersion()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(SmppConstants.VERSION_3_4)
	 * </pre>
	 * 
	 * <pre>
	 * .interfaceVersion(SmppConstants.VERSION_5_0)
	 * .interfaceVersion()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(SmppConstants.VERSION_3_4)
	 * </pre>
	 * 
	 * In both cases, {@code interfaceVersion(SmppConstants.VERSION_5_0)} is
	 * used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param version
	 *            the version of the SMPP protocol
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder interfaceVersion(Byte version) {
		interfaceVersionValueBuilder.setValue(InterfaceVersion.fromValue(version));
		return this;
	}

	/**
	 * The SMPP protocol version (one of {@link InterfaceVersion#VERSION_3_3},
	 * {@link InterfaceVersion#VERSION_3_4},
	 * {@link InterfaceVersion#VERSION_5_0}).
	 * 
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .interfaceVersion()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(InterfaceVersion.VERSION_3_4)
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #interfaceVersion(InterfaceVersion)}
	 * takes precedence over property values and default value.
	 * 
	 * <pre>
	 * .interfaceVersion(InterfaceVersion.VERSION_5_0)
	 * .interfaceVersion()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(InterfaceVersion.VERSION_3_4)
	 * </pre>
	 * 
	 * The value {@code InterfaceVersion.VERSION_5_0} is used regardless of the
	 * value of the properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, InterfaceVersion> interfaceVersion() {
		return interfaceVersionValueBuilder;
	}

	/**
	 * The bind command type (see {@link SmppBindType}).
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #bindType()}.
	 * 
	 * <pre>
	 * .bindType(SmppBindType.TRANSCEIVER)
	 * .bindType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(SmppBindType.RECEIVER)
	 * </pre>
	 * 
	 * <pre>
	 * .bindType(SmppBindType.TRANSCEIVER)
	 * .bindType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(SmppBindType.RECEIVER)
	 * </pre>
	 * 
	 * In both cases, {@code bindType(SmppBindType.TRANSCEIVER)} is used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If {@code null} value is set, it is like not setting a value at all. The
	 * property/default value configuration is applied.
	 * 
	 * @param bindType
	 *            the bind type
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder bindType(SmppBindType bindType) {
		bindTypeValueBuilder.setValue(bindType);
		return this;
	}

	/**
	 * The bind command type (see {@link SmppBindType}).
	 * 
	 * <p>
	 * This method is mainly used by {@link Configurer}s to register some
	 * property keys and/or a default value. The aim is to let developer be able
	 * to externalize its configuration (using system properties, configuration
	 * file or anything else). If the developer doesn't configure any value for
	 * the registered properties, the default value is used (if set).
	 * 
	 * <pre>
	 * .bindType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(SmppBindType.RECEIVER)
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #bindType(SmppBindType)} takes precedence
	 * over property values and default value.
	 * 
	 * <pre>
	 * .bindType(SmppBindType.TRANSCEIVER)
	 * .bindType()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(SmppBindType.RECEIVER)
	 * </pre>
	 * 
	 * The value {@code SmppBindType.TRANSCEIVER} is used regardless of the
	 * value of the properties and default value.
	 * 
	 * <p>
	 * See {@link ConfigurationValueBuilder} for more information.
	 * 
	 * 
	 * @return the builder to configure property keys/default value
	 */
	public ConfigurationValueBuilder<CloudhopperBuilder, SmppBindType> bindType() {
		return bindTypeValueBuilder;
	}

	/**
	 * Configures how Cloudhopper will encode SMS messages. Charsets defined by
	 * the SMPP protocol may be different from NIO charsets.
	 * 
	 * <p>
	 * The encoder will be used to transform Java {@link String} into a byte
	 * array that is understandable by SMPP servers.
	 * 
	 * <p>
	 * This builder configures encoders for both messages that are split and
	 * message that are not split.
	 * 
	 * <p>
	 * This builder allows to configure:
	 * <ul>
	 * <li>Enable/disable the standard GSM encoders (GSM 7-bit, GSM 8-bit and
	 * UCS-2) as defined in
	 * <a href="https://en.wikipedia.org/wiki/GSM_03.38">GSM 03.38
	 * specification</a>. It also allows to define different priority order</li>
	 * <li>Enable/disable automatic guessing of encoding (based on previously
	 * registered priorities).</li>
	 * <li>Define a fallback encoder based on {@link Charset}</li>
	 * <li>Provide custom {@link Encoder}s</li>
	 * </ul>
	 * 
	 * <pre>
	 * {@code
	 * .encoder()
	 *    .gsm7()
	 *      .properties("${ogham.sms.cloudhopper.encoder.gsm7bit-packed.priority}", "${ogham.sms.smpp.encoder.gsm7bit-packed.priority}")
	 *      .defaultValue(100000)
	 *      .and()
	 *    .gsm8()
	 *      .properties("${ogham.sms.cloudhopper.encoder.gsm8bit.priority}", "${ogham.sms.smpp.encoder.gsm8bit.priority}")
	 *      .defaultValue(99000)
	 *      .and()
	 *    .ucs2()
	 *      .properties("${ogham.sms.cloudhopper.encoder.ucs2.priority}", "${ogham.sms.smpp.encoder.ucs2.priority}")
	 *      .defaultValue(98000)
	 *      .and()
	 *    .autoGuess()
	 *      .properties("${ogham.sms.cloudhopper.encoder.auto-guess.enable}", "${ogham.sms.smpp.encoder.auto-guess.enable}")
	 *      .defaultValue(true)
	 *      .and()
	 *    .fallback()
	 *      .properties("${ogham.sms.cloudhopper.encoder.default-charset}", "${ogham.sms.smpp.encoder.default-charset}")
	 *      .defaultValue(CharsetUtil.NAME_GSM)
	 *      .and()
	 *    .customEncoder(new MyCustomEncoder(), 50000)
	 * }
	 * </pre>
	 * 
	 * @return the builder to configure the encoder
	 */
	public EncoderBuilder encoder() {
		if (encoderBuilder == null) {
			encoderBuilder = new EncoderBuilder(this, buildContext);
			sharedEncoderBuilder.update(encoderBuilder);
		}
		return encoderBuilder;
	}

	/**
	 * Configures how Cloudhopper will split messages.
	 * 
	 * <p>
	 * The splitter will check if the whole message can fit in a single segment.
	 * If not the splitter will split the whole message in several segments with
	 * a header to indicate splitting information such as number of segments,
	 * reference number and current segment number.
	 * 
	 * <p>
	 * {@link Encoder} configured using {@link #encoder()} is used to encode
	 * each segment.
	 * 
	 * <p>
	 * If automatic guessing of best standard encoder is enabled for
	 * {@link Encoder} (using {@code encoder().autoGuess(true)}), and message
	 * splitting is enabled, then standard message splitting is configured such
	 * as:
	 * <ul>
	 * <li>If GSM 7-bit encoder is enabled, {@link GsmMessageSplitter} is used
	 * to split messages that support this encoding. If whole message can fit in
	 * a single segment of 160 characters. Longer message is split into segments
	 * of either 153 characters or 152 characters (depending on reference number
	 * generation, see {@link ReferenceNumberGenerator})</li>
	 * <li>If GSM 8-bit encoder is enabled, {@link GsmMessageSplitter} is used
	 * to split messages that support this encoding. If whole message can fit in
	 * a single segment of 140 characters. Longer message is split into segments
	 * of either 134 characters or 133 characters (depending on reference number
	 * generation, see {@link ReferenceNumberGenerator})</li>
	 * <li>If UCS-2 encoder is enabled, {@link GsmMessageSplitter} is used to
	 * split messages that support this encoding. If whole message can fit in a
	 * single segment of 70 characters. Longer message is split into segments of
	 * either 67 characters or 66 characters (depending on reference number
	 * generation, see {@link ReferenceNumberGenerator})</li>
	 * </ul>
	 * 
	 * Each registered splitter uses the same priority as associated
	 * {@link Encoder}.
	 * 
	 * If you don't want standard message splitting based on supported
	 * {@link Encoder}s, you can either disable message splitting or provide a
	 * custom splitter with higher priority.
	 * 
	 * <p>
	 * This builder allows to configure:
	 * <ul>
	 * <li>Enable/disable message splitting</li>
	 * <li>Provide a custom split strategy</li>
	 * <li>Choose strategy for reference number generation</li>
	 * </ul>
	 * 
	 * <p>
	 * Examples of usage:
	 * 
	 * <pre>
	 * {@code
	 * .splitter()
	 *   .enable()
	 *     .properties("${ogham.sms.cloudhopper.split.enable}", "${ogham.sms.smpp.split.enable}", "${ogham.sms.split.enable}")
	 *     .defaultValue(true)
	 *     .and()
	 *   .customSplitter(new MyCustomSplitter(), 100000)
	 *   .referenceNumber()
	 *     .random()
	 *     .random(new Random())
	 *     .generator(new MyCustomReferenceNumberGenerator())
	 * }
	 * </pre>
	 * 
	 * @return the builder to configure message splitting
	 */
	public MessageSplitterBuilder splitter() {
		if (messageSplitterBuilder == null) {
			messageSplitterBuilder = new MessageSplitterBuilder(this, buildContext, sharedEncoderBuilder);
		}
		return messageSplitterBuilder;
	}

	/**
	 * Configures Cloudhopper session management (timeouts, retry, session
	 * name...).
	 * 
	 * @return the builder to configure the session management
	 */
	public SessionBuilder session() {
		if (sessionBuilder == null) {
			sessionBuilder = new SessionBuilder(this, buildContext);
		}
		return sessionBuilder;
	}

	/**
	 * Overrides any previously defined Cloudhopper parameters to use the
	 * provided session.
	 * 
	 * <p>
	 * If this method is called several times, only the last session is used.
	 * 
	 * @param session
	 *            the Cloudhopper session to use
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder session(ExtendedSmppSessionConfiguration session) {
		this.sessionConfiguration = session;
		return this;
	}

	/**
	 * The address_range parameter is used in the bind_receiver and
	 * bind_transceiver command to specify a set of SME addresses serviced by
	 * the ESME client. A single SME address may also be specified in the
	 * address_range parameter. UNIX Regular Expression notation should be used
	 * to specify a range of addresses. Messages addressed to any destination in
	 * this range shall be routed to the ESME.
	 * 
	 * Default to {@code null}.
	 * 
	 * Note: For IP addresses, it is only possible to specify a single IP
	 * address. A range of IP addresses are not allowed. IP version 6.0 is not
	 * currently supported in this version of the protocol.
	 * 
	 * Note: It is likely that the addr_range field is not supported or
	 * deliberately ignored on most Message Centres. The reason for this is that
	 * most carriers will not allow an ESME control the message routing as this
	 * can carry the risk of mis-routing messages. In such circumstances, the
	 * ESME will be requested to set the field to NULL.
	 * 
	 * @param range
	 *            the address range
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder addressRange(Address range) {
		this.addressRange = range;
		return this;
	}

	/**
	 * Enable or disable SSL configuration and configure how SSL is handled.
	 * 
	 * See <a href=
	 * "https://github.com/fizzed/cloudhopper-smpp/blob/master/SSL.md">How to
	 * use SSL with cloudhopper-smpp</a>
	 * 
	 * @return the builder to configure SSL
	 */
	public SslBuilder ssl() {
		if (sslBuilder == null) {
			sslBuilder = new SslBuilder(this, buildContext);
		}
		return sslBuilder;
	}

	/**
	 * Configure logs:
	 * <ul>
	 * <li>Enable/disable log of {@link Pdu}s</li>
	 * <li>Enable/disable log of bytes</li>
	 * </ul>
	 * 
	 * @return the builder to enable/disable some logs
	 */
	public LoggingBuilder logging() {
		if (loggingBuilder == null) {
			loggingBuilder = new LoggingBuilder(this);
		}
		return loggingBuilder;
	}

	/**
	 * By default, {@link CloudhopperSMPPSender} uses {@link DefaultSmppClient}
	 * client. This option provides a way to use another {@link SmppClient}.
	 * 
	 * @param supplier
	 *            an implementation that provides an instance of a
	 *            {@link SmppClient}
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder clientSupplier(SmppClientSupplier supplier) {
		this.clientSupplier = supplier;
		return this;
	}

	/**
	 * By default, {@link CloudhopperSMPPSender} uses
	 * {@link DefaultSmppSessionHandler}. This option provides a way to use
	 * another {@link SmppSessionHandler}.
	 * 
	 * @param supplier
	 *            an implementation that provides an instance of a
	 *            {@link SmppSessionHandler}
	 * @return this instance for fluent chaining
	 */
	public CloudhopperBuilder smppSessionHandlerSupplier(SmppSessionHandlerSupplier supplier) {
		this.smppSessionHandler = supplier;
		return this;
	}

	/**
	 * {@link Sms} message is converted to {@link SubmitSm}(s) using a
	 * {@link MessagePreparator}.
	 * 
	 * <p>
	 * You can provide a custom {@link MessagePreparator} instance if the
	 * default behavior doesn't fit your needs.
	 * </p>
	 * 
	 * <p>
	 * If a custom {@link MessagePreparator} is set, any other preparator (using
	 * {@link #userData()}) is not used.
	 * </p>
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * </p>
	 * 
	 * <p>
	 * If {@code null} value is provided, then custom {@link MessagePreparator}
	 * is disabled. Other configured preparators are used (using
	 * {@link #userData()}).
	 * </p>
	 * 
	 * @param preparator
	 *            the custom preprator instance
	 * @return this instance for fluent chaining
	 * @see #userData()
	 */
	public CloudhopperBuilder messagePreparator(MessagePreparator preparator) {
		this.preparator = preparator;
		return this;
	}

	/**
	 * SMS message (named "User Data" in SMPP specification) can be transmitted
	 * using:
	 * <ul>
	 * <li>Either {@code short_message} field (standard field for "User
	 * Data").</li>
	 * <li>Or {@code message_payload} optional parameter.</li>
	 * </ul>
	 * 
	 * <p>
	 * This builder allow to configure which strategy to use for sending
	 * message:
	 * <ul>
	 * <li>Either use {@code short_message} field</li>
	 * <li>Or use {@code message_payload} field</li>
	 * </ul>
	 * 
	 * <p>
	 * The result of {@link #userData()} configuration affects the message
	 * preparation strategy.
	 * </p>
	 * 
	 * <p>
	 * Examples of usage:
	 * 
	 * <pre>
	 * {@code
	 * .userData()
	 *   .useShortMessage()
	 *     .properties("${ogham.sms.cloudhopper.user-data.use-short-message}", "${ogham.sms.smpp.user-data.use-short-message}")
	 *     .defaultValue(true)
	 *     .and()
	 *   .useTlvMessagePayload()
	 *     .properties("${ogham.sms.cloudhopper.user-data.use-tlv-message-payload}", "${ogham.sms.smpp.user-data.use-tlv-message-payload}")
	 *     .defaultValue(false)
	 * }
	 * </pre>
	 * 
	 * If any of {@code ogham.sms.cloudhopper.user-data.use-short-message}
	 * property or {@code ogham.sms.user-data.use-short-message} property is set
	 * to true, it uses {@code short_message} field.
	 * 
	 * If any of {@code ogham.sms.cloudhopper.user-data.use-tlv-message-payload}
	 * property or {@code ogham.sms.user-data.use-tlv-message-payload} property
	 * is set to true, it uses {@code message_payload} field.
	 * 
	 * If none of the above properties is set, it uses {@code short_message}
	 * field is used (last value of {@code shortMessage} is set to
	 * {@code "true"}).
	 * 
	 * <p>
	 * If {@link #userData()} is not configured at all, then default behavior is
	 * used ({@code short_message} field is used).
	 * </p>
	 * 
	 * @return the builder to configure how the "User Data" is sent
	 */
	public UserDataBuilder userData() {
		if (userDataBuilder == null) {
			userDataBuilder = new UserDataBuilder(this, buildContext);
		}
		return userDataBuilder;
	}

	/**
	 * Data Coding Scheme is a one-octet field in Short Messages (SM) and Cell
	 * Broadcast Messages (CB) which carries a basic information how the
	 * recipient handset should process the received message. The information
	 * includes:
	 * <ul>
	 * <li>the character set or message coding which determines the encoding of
	 * the message user data</li>
	 * <li>the message class which determines to which component of the Mobile
	 * Station (MS) or User Equipment (UE) should be the message delivered</li>
	 * <li>the request to automatically delete the message after reading</li>
	 * <li>the state of flags indicating presence of unread voicemail, fax,
	 * e-mail or other messages</li>
	 * <li>the indication that the message content is compressed</li>
	 * <li>the language of the cell broadcast message</li>
	 * </ul>
	 * The field is described in 3GPP 23.040 and 3GPP 23.038 under the name
	 * TP-DCS (see <a href=
	 * "https://en.wikipedia.org/wiki/Data_Coding_Scheme#SMS_Data_Coding_Scheme">SMS
	 * Data Coding Scheme</a>).
	 * 
	 * SMPP 3.4 introduced a new list of {@code data_coding} values (see
	 * <a href="https://en.wikipedia.org/wiki/Short_Message_Peer-to-Peer">Short
	 * Message Peer to Peer</a>).
	 * 
	 * <p>
	 * This builder allows to configure how Data Coding Scheme value is
	 * determined:
	 * <ul>
	 * <li>Use automatic mode base on interface version (see
	 * {@link #interfaceVersion(InterfaceVersion)} and
	 * {@link #interfaceVersion(Byte)}) and charset encoding (see
	 * {@link #encoder()}) used to encode the message ("User Data")</li>
	 * <li>Use a fixed value used for every message</li>
	 * <li>Use a custom implementation</li>
	 * </ul>
	 * 
	 * <p>
	 * Examples of usage:
	 * 
	 * <pre>
	 * {@code
	 * .dataCodingScheme()
	 *   .auto("${ogham.sms.cloudhopper.data-coding-scheme.auto.enable}", "${ogham.sms.smpp.data-coding-scheme.auto.enable}")
	 *   .value("${ogham.sms.cloudhopper.data-coding-scheme.value}", "${ogham.sms.smpp.data-coding-scheme.value}")
	 *   .custom(new MyCustomDataCodingProvider())
	 * }
	 * </pre>
	 * 
	 * See {@link DataCodingSchemeBuilder#auto(Boolean)},
	 * {@link DataCodingSchemeBuilder#value(Byte)} and
	 * {@link DataCodingSchemeBuilder#custom(DataCodingProvider)} for more
	 * information.
	 * 
	 * 
	 * @return the builder to configure how to determine Data Coding Scheme
	 *         value
	 */
	public DataCodingSchemeBuilder dataCodingScheme() {
		if (dataCodingBuilder == null) {
			dataCodingBuilder = new DataCodingSchemeBuilder(this, buildContext, this::getInterfaceVersion);
		}
		return dataCodingBuilder;
	}

	@Override
	public CloudhopperSMPPSender build() {
		CloudhopperSessionOptions sessionOpts = buildSessionOpts();
		ExtendedSmppSessionConfiguration configuration = buildSessionConfiguration(sessionOpts);
		if (configuration.getHost() == null || configuration.getPort() == 0) {
			return null;
		}
		LOG.info("Sending SMS using Cloudhopper is registered");
		LOG.debug("SMPP server address: {}:{}", configuration.getHost(), configuration.getPort());
		SessionHandlingStrategy sessionHandler = buildSessionHandlingStrategy(configuration);
		return buildContext.register(new CloudhopperSMPPSender(configuration, sessionHandler, buildPreparator()));
	}

	private CloudhopperSessionOptions buildSessionOpts() {
		if (sessionBuilder != null) {
			return sessionBuilder.build();
		}
		CloudhopperSessionOptions cloudhopperSessionOptions = buildContext.register(new CloudhopperSessionOptions());
		cloudhopperSessionOptions.setConnectRetry(buildConnectRetry(cloudhopperSessionOptions));
		return cloudhopperSessionOptions;
	}

	private SessionHandlingStrategy buildSessionHandlingStrategy(ExtendedSmppSessionConfiguration configuration) {
		if (configuration.getKeepAlive() != null && configuration.getKeepAlive().isEnable(false)) {
			return buildKeepAliveHandler(configuration);
		}
		if (configuration.getReuseSession() != null && configuration.getReuseSession().isEnable(false)) {
			return buildReuseSessionHandler(configuration);
		}
		return buildAlwaysNewSessionHandler(configuration);
	}

	private SessionHandlingStrategy buildKeepAliveHandler(ExtendedSmppSessionConfiguration configuration) {
		return new KeepSessionAliveStrategy(configuration, buildClientSupplier(), buildSmppSessionHandler(), configuration.getConnectRetry(), configuration.getKeepAlive().getExecutor(), buildKeepAliveErrorAnalyzer(configuration.getKeepAlive()), buildReconnectionErrorHandler());
	}
	
	private ErrorAnalyzer buildKeepAliveErrorAnalyzer(KeepAliveOptions options) {
		return new DefaultErrorAnalyzer(options.getMaxConsecutiveTimeouts());
	}
	
	private ErrorHandler buildReconnectionErrorHandler() {
		// TODO: make this configurable ?
		return new LogErrorHandler("Failed to reconnect", Level.ERROR);
	}

	private SessionHandlingStrategy buildReuseSessionHandler(ExtendedSmppSessionConfiguration configuration) {
		return new MayReuseSessionStrategy(configuration, buildClientSupplier(), buildSmppSessionHandler(), configuration.getConnectRetry(), buildReuseSessionErrorAnalyzer());
	}
	
	private ErrorAnalyzer buildReuseSessionErrorAnalyzer() {
		return new DefaultErrorAnalyzer(1);
	}

	private SessionHandlingStrategy buildAlwaysNewSessionHandler(ExtendedSmppSessionConfiguration configuration) {
		return new AlwaysNewSessionStrategy(configuration, buildClientSupplier(), buildSmppSessionHandler(), configuration.getConnectRetry());
	}

	private RetryExecutor buildConnectRetry(CloudhopperSessionOptions sessionOpts) {
		if (sessionOpts.getConnectRetry() == null) {
			return noRetry();
		}
		return sessionOpts.getConnectRetry();
	}
	
	private SimpleRetryExecutor noRetry() {
		return buildContext.register(new SimpleRetryExecutor(() -> null, buildContext.register(new ThreadSleepAwaiter())));
	}

	private MessagePreparator buildPreparator() {
		if (preparator != null) {
			return preparator;
		}
		if (userDataBuilder != null) {
			UserDataPropValues values = userDataBuilder.build();
			if (values.isUseShortMessage()) {
				return buildShortMessagePreparator();
			}
			if (values.isUseTlvMessagePayload()) {
				return buildTlvMessagePayloadMessagePreparator();
			}
		}
		return buildShortMessagePreparator();
	}

	private MessagePreparator buildShortMessagePreparator() {
		return buildContext.register(new ShortMessagePreparator(buildSplitter(buildEncoder()), buildDataCodingProvider(), buildPhoneNumberTranslator()));
	}

	private MessagePreparator buildTlvMessagePayloadMessagePreparator() {
		return buildContext.register(new TlvMessagePayloadMessagePreparator(buildSplitter(buildEncoder()), buildDataCodingProvider(), buildPhoneNumberTranslator()));
	}

	private Encoder buildEncoder() {
		if (encoderBuilder == null) {
			return buildContext.register(new CloudhopperCharsetSupportingEncoder(NamedCharset.from(DEFAULT_CHARSET)));
		}
		return encoderBuilder.build();
	}

	private DataCodingProvider buildDataCodingProvider() {
		if (dataCodingBuilder == null) {
			return buildContext.register(new CharsetMapToCharacterEncodingGroupDataCodingProvider(true));
		}
		return dataCodingBuilder.build();
	}

	private MessageSplitter buildSplitter(Encoder encoder) {
		if (messageSplitterBuilder == null) {
			return buildContext.register(new NoSplitMessageSplitter(encoder));
		}
		MessageSplitter splitter = messageSplitterBuilder.build();
		if (splitter != null) {
			return splitter;
		}
		return buildContext.register(new NoSplitMessageSplitter(encoder));
	}

	private SmppClientSupplier buildClientSupplier() {
		if (clientSupplier == null) {
			return buildContext.register(DefaultSmppClient::new);
		}
		return clientSupplier;
	}

	private SmppSessionHandlerSupplier buildSmppSessionHandler() {
		if (smppSessionHandler == null) {
			return defaultSmppSessionHandlerSupplier();
		}
		return smppSessionHandler;
	}
	
	private SmppSessionHandlerSupplier defaultSmppSessionHandlerSupplier() {
		return () -> new RespondToEnquireLinkRequestHandler(new RespondToDeliveryReceiptHandler(new DefaultSmppSessionHandler()));
	}

	private PhoneNumberTranslator buildPhoneNumberTranslator() {
		// TODO: allow configuration of fallback phone number translator
		return buildContext.register(new CompositePhoneNumberTranslator(buildContext.register(new DefaultHandler())));
	}

	private ExtendedSmppSessionConfiguration buildSessionConfiguration(CloudhopperSessionOptions sessionOpts) {
		ExtendedSmppSessionConfiguration session = buildContext.register(new ExtendedSmppSessionConfiguration());
		ExtendedSmppSessionConfiguration manual = sessionConfiguration == null ? new ExtendedSmppSessionConfiguration() : sessionConfiguration;
		// @formatter:off
		merge(session::setType,                    bindTypeValueBuilder::getValue,            manual::getType,                                           () -> DEFAULT_BIND_TYPE);
		merge(session::setHost,                    hostValueBuilder::getValue,                manual::getHost);
		merge(session::setPort,                    portValueBuilder::getValue,                manual::getPort,                                           () -> 0);
		merge(session::setSystemId,                systemIdValueBuilder::getValue,            manual::getSystemId);
		merge(session::setPassword,                passwordValueBuilder::getValue,            manual::getPassword);
		merge(session::setSystemType,              systemTypeValueBuilder::getValue,          manual::getSystemType);
		merge(session::setBindTimeout,             sessionOpts::getBindTimeout,               considerZeroAsDefault(manual::getBindTimeout));
		merge(session::setConnectTimeout,          sessionOpts::getConnectTimeout,            considerZeroAsDefault(manual::getConnectTimeout));
		merge(session::setInterfaceVersion,        this::getInterfaceVersion,                 manual::getInterfaceVersion);
		merge(session::setName,                    sessionOpts::getSessionName,               manual::getName);
		merge(session::setRequestExpiryTimeout,    sessionOpts::getRequestExpiryTimeout,      considerZeroAsDefault(manual::getRequestExpiryTimeout));
		merge(session::setWindowMonitorInterval,   sessionOpts::getWindowMonitorInterval,     considerZeroAsDefault(manual::getWindowMonitorInterval));
		merge(session::setWindowSize,              sessionOpts::getWindowSize,                considerZeroAsDefault(manual::getWindowSize));
		merge(session::setWindowWaitTimeout,       sessionOpts::getWindowWaitTimeout,         considerZeroAsDefault(manual::getWindowWaitTimeout));
		merge(session::setWriteTimeout,            sessionOpts::getWriteTimeout,              considerZeroAsDefault(manual::getWriteTimeout));
		merge(session::setResponseTimeout,         sessionOpts::getResponseTimeout,           considerZeroAsDefault(manual::getResponseTimeout),         () -> DEFAULT_RESPONSE_TIMEOUT);
		merge(session::setUnbindTimeout,           sessionOpts::getUnbindTimeout,             considerZeroAsDefault(manual::getUnbindTimeout),           () -> DEFAULT_UNBIND_TIMEOUT);
		merge(session::setReuseSession,            sessionOpts::getReuseSession,              manual::getReuseSession);
		merge(session::setAddressRange,            () -> addressRange,                        manual::getAddressRange);
		merge(session::setKeepAlive,               sessionOpts::getKeepAlive,                 manual::getKeepAlive);
		merge(session::setConnectRetry,            () -> buildConnectRetry(sessionOpts),      manual::getConnectRetry);
		// @formatter:on
		configureSsl(session);
		configureLogs(session);
		return session;
	}

	private static <T> Supplier<T> considerZeroAsDefault(Supplier<T> getter) {
		T value = getter.get();
		if (value instanceof Long) {
			return () -> (Long) value == 0 ? null : value;
		}
		if (value instanceof Integer) {
			return () -> (Integer) value == 0 ? null : value;
		}
		return getter;
	}
	
	@SafeVarargs
	private static <T> void merge(Consumer<T> setter, Supplier<T>... getters) {
		T value = null;
		for (Supplier<T> getter : getters) {
			value = getter.get();
			if (value != null) {
				break;
			}
		}
		// if there is a value, set it
		if (value != null) {
			setter.accept(value);
		}
	}

	private void configureLogs(SmppSessionConfiguration session) {
		if (loggingBuilder == null) {
			return;
		}
		LoggingOptions options = loggingBuilder.build();
		if (options != null) {
			session.setLoggingOptions(options);
		}
	}

	private void configureSsl(SmppSessionConfiguration session) {
		if (sslBuilder == null) {
			return;
		}
		SslConfiguration sslConfiguration = sslBuilder.build();
		session.setUseSsl(sslConfiguration != null);
		if (sslConfiguration != null) {
			session.setSslConfiguration(sslConfiguration);
		}
	}


	private Byte getInterfaceVersion() {
		InterfaceVersion version = interfaceVersionValueBuilder.getValue(DEFAULT_INTERFACE_VERSION);
		return version.value();
	}

}