SmsBuilder.java
package fr.sii.ogham.sms.builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import fr.sii.ogham.core.async.Awaiter;
import fr.sii.ogham.core.builder.ActivableAtRuntime;
import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.builder.condition.RequiredClass;
import fr.sii.ogham.core.builder.condition.RequiredClasses;
import fr.sii.ogham.core.builder.condition.RequiredProperties;
import fr.sii.ogham.core.builder.condition.RequiredProperty;
import fr.sii.ogham.core.builder.configurer.MessagingConfigurer;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.retry.RetryBuilder;
import fr.sii.ogham.core.builder.sender.SenderImplementationBuilderHelper;
import fr.sii.ogham.core.builder.template.DetectorBuilder;
import fr.sii.ogham.core.builder.template.TemplateBuilderHelper;
import fr.sii.ogham.core.builder.template.VariantBuilder;
import fr.sii.ogham.core.condition.Condition;
import fr.sii.ogham.core.condition.fluent.MessageConditions;
import fr.sii.ogham.core.filler.MessageFiller;
import fr.sii.ogham.core.fluent.AbstractParent;
import fr.sii.ogham.core.message.content.MultiTemplateContent;
import fr.sii.ogham.core.message.content.Variant;
import fr.sii.ogham.core.retry.ExponentialDelayRetry;
import fr.sii.ogham.core.retry.FixedDelayRetry;
import fr.sii.ogham.core.retry.FixedIntervalRetry;
import fr.sii.ogham.core.retry.PerExecutionDelayRetry;
import fr.sii.ogham.core.retry.RetryExecutor;
import fr.sii.ogham.core.sender.AutoRetrySender;
import fr.sii.ogham.core.sender.ConditionalSender;
import fr.sii.ogham.core.sender.ContentTranslatorSender;
import fr.sii.ogham.core.sender.FillerSender;
import fr.sii.ogham.core.sender.MessageSender;
import fr.sii.ogham.core.template.parser.TemplateParser;
import fr.sii.ogham.core.translator.content.ContentTranslator;
import fr.sii.ogham.core.translator.content.EveryContentTranslator;
import fr.sii.ogham.core.translator.content.TemplateContentTranslator;
import fr.sii.ogham.core.util.BuilderUtils;
import fr.sii.ogham.sms.message.PhoneNumber;
import fr.sii.ogham.sms.message.Sms;
import fr.sii.ogham.sms.message.addressing.AddressedPhoneNumber;
import fr.sii.ogham.sms.sender.PhoneNumberTranslatorSender;
import fr.sii.ogham.sms.sender.SmsSender;
/**
* Configures how to send {@link Sms} messages. It allows to:
* <ul>
* <li>register and configure several sender implementations</li>
* <li>register and configure several template engines for parsing templates as
* message content</li>
* <li>configure handling of missing {@link Sms} information</li>
* <li>configure number format handling</li>
* </ul>
*
* You can send a {@link Sms} using the minimal behavior and using Cloudhopper
* implementation:
*
* <pre>
* <code>
* // Instantiate the messaging service
* MessagingService service = new MessagingBuilder()
* .sms()
* .sender(CloudhopperBuilder.class) // enable SMS sending using Cloudhopper
* .host("your SMPP server host")
* .port("your SMPP server port")
* .systemId("your SMPP system_id")
* .password("an optional password")
* .and()
* .and()
* .build();
* // send the sms
* service.send(new Sms()
* .from("sender phone number")
* .content("sms content")
* .to("recipient phone number"));
* </code>
* </pre>
*
* You can also send a {@link Sms} using a template (using Freemarker for
* example):
*
* The Freemarker template ("sms/sample.txt.ftl"):
*
* <pre>
* Sms content with variables: ${name} ${value}
* </pre>
*
* Then you can send the {@link Sms} like this:
*
* <pre>
* <code>
* // Instantiate the messaging service
* MessagingService service = new MessagingBuilder()
* .sms()
* .sender(CloudhopperBuilder.class) // enable SMS sending using Cloudhopper
* .host("your SMPP server host")
* .port("your SMPP server port")
* .systemId("your SMPP system_id")
* .password("an optional password")
* .and()
* .and()
* .template(FreemarkerSmsBuilder.class) // enable templating using Freemarker
* .classpath()
* .lookup("classpath:") // search resources/templates in the classpath if a path is prefixed by "classpath:"
* .and()
* .and()
* .build();
* // send the sms
* service.send(new Sms()
* .from("sender phone number")
* .content(new TemplateContent("classpath:sms/sample.txt.ftl", new SampleBean("foo", 42)))
* .to("recipient phone number"));
* </code>
* </pre>
*
* <p>
* Instead of explicitly configures SMPP host/port/system_id/password in your
* code, it could be better to externalize the configuration in a properties
* file for example (for example a file named "sms.properties" in the
* classpath). The previous example becomes:
*
* <pre>
* <code>
* // Instantiate the messaging service
* MessagingService service = new MessagingBuilder()
* .environment()
* .properties("sms.properties")
* .and()
* .sms()
* .sender(CloudhopperBuilder.class) // enable SMS sending using Cloudhopper
* .host("${smpp.host}")
* .port("${smpp.port}")
* .systemId("${smpp.system-id}")
* .password("${smpp.password}")
* .and()
* .and()
* .template(FreemarkerSmsBuilder.class) // enable templating using Freemarker
* .classpath()
* .lookup("classpath:") // search resources/templates in the classpath if a path is prefixed by "classpath:"
* .and()
* .and()
* .build();
* // send the sms
* service.send(new Sms()
* .from("sender phone number")
* .content(new TemplateContent("classpath:sms/sample.txt.ftl", new SampleBean("foo", 42)))
* .to("recipient phone number"));
* </code>
* </pre>
*
* The content of the file "sms.properties":
*
* <pre>
* smpp.host=your SMPP server host
* smpp.port=your SMPP server port
* smpp.system-id=your SMPP system_id
* smpp.password=an optional password
* </pre>
*
*
* Some fields of the SMS may be automatically filled by a default value if they
* are not defined. For example, the sender phone number could be configured
* only once for your application:
*
* <pre>
* <code>
* // Instantiate the messaging service
* MessagingService service = new MessagingBuilder()
* .environment()
* .properties("sms.properties")
* .and()
* .sms()
* .sender(CloudhopperBuilder.class) // enable SMS sending using Cloudhopper
* .host().properties("${smpp.host}").and()
* .port().properties("${smpp.port}").and()
* .systemId().properties("${smpp.system-id}").and()
* .password().properties("${smpp.password}").and()
* .and()
* .autofill() // enables and configures autofilling
* .from()
* .defaultValue().properties("${sms.sender.number}").and()
* .and()
* .and()
* .and()
* .template(FreemarkerSmsBuilder.class) // enable templating using Freemarker
* .classpath()
* .lookup("classpath:") // search resources/templates in the classpath if a path is prefixed by "classpath:"
* .and()
* .and()
* .build();
* // send the sms (now the sender phone number can be omitted)
* service.send(new Sms()
* .content(new TemplateContent("classpath:sms/sample.txt.ftl", new SampleBean("foo", 42)))
* .to("recipient phone number"));
* </code>
* </pre>
*
* The new content of the file "sms.properties":
*
* <pre>
* smpp.host=your SMPP server host
* smpp.port=your SMPP server port
* smpp.system-id=your SMPP system_id
* smpp.password=an optional password
* sms.sender.number=the sender phone number
* </pre>
*
* <p>
* All the previous examples are provided to understand what can be configured.
* Hopefully, Ogham provides auto-configuration with a default behavior that
* fits 95% of usages. This auto-configuration is provided by
* {@link MessagingConfigurer}s. Those configurers are automatically applied
* when using predefined {@link MessagingBuilder}s like
* {@link MessagingBuilder#minimal()} and {@link MessagingBuilder#standard()}.
*
* The previous sample using standard configuration becomes:
*
* <pre>
* <code>
* // Instantiate the messaging service
* MessagingService service = MessagingBuilder.standard()
* .environment()
* .properties("sms.properties")
* .and()
* .build();
* // send the sms
* service.send(new Sms()
* .content(new TemplateContent("classpath:sms/sample.txt.ftl", new SampleBean("foo", 42)))
* .to("recipient phone number"));
* </code>
* </pre>
*
* The new content of the file "sms.properties":
*
* <pre>
* ogham.sms.smpp.host=your SMPP server host
* ogham.sms.smpp.port=your SMPP server port
* ogham.sms.smpp.system-id=your SMPP system_id
* ogham.sms.smpp.password=an optional password
* ogham.sms.from.default-value=the sender phone number
* </pre>
*
* <p>
* You can also use the auto-configuration for benefit from default behaviors
* and override some behaviors for your needs:
*
* <pre>
* <code>
* // Instantiate the messaging service
* MessagingService service = MessagingBuilder.standard()
* .environment()
* .properties("sms.properties")
* .and()
* .sms()
* .autofill()
* .from()
* .defaultValue().properties("${sms.sender.number}").and() // overrides default sender phone number property
* .and()
* .and()
* .and()
* .build();
* // send the sms
* service.send(new Sms()
* .content(new TemplateContent("classpath:sms/sample.txt.ftl", new SampleBean("foo", 42)))
* .to("recipient phone number"));
* </code>
* </pre>
*
* The new content of the file "sms.properties":
*
* <pre>
* ogham.sms.smpp.host=your SMPP server host
* ogham.sms.smpp.port=your SMPP server port
* ogham.sms.smpp.system-id=your SMPP system_id
* ogham.sms.smpp.password=an optional password
* sms.sender.number=the sender phone number
* </pre>
*
* @author Aurélien Baudet
*
*/
public class SmsBuilder extends AbstractParent<MessagingBuilder> implements Builder<ConditionalSender> {
private static final Logger LOG = LoggerFactory.getLogger(SmsBuilder.class);
private final BuildContext buildContext;
private final TemplateBuilderHelper<SmsBuilder> templateBuilderHelper;
private final SenderImplementationBuilderHelper<SmsBuilder> senderBuilderHelper;
private AutofillSmsBuilder autofillBuilder;
private PhoneNumbersBuilder phoneNumbersBuilder;
private RetryBuilder<SmsBuilder> retryBuilder;
/**
* Initializes the builder with a parent builder. The parent builder is used
* when calling {@link #and()} method. The {@link BuildContext} is used to
* evaluate properties when {@link #build()} method is called.
*
* @param parent
* the parent builder
* @param buildContext
* for registering instances and property evaluation
*/
public SmsBuilder(MessagingBuilder parent, BuildContext buildContext) {
super(parent);
this.buildContext = buildContext;
templateBuilderHelper = new TemplateBuilderHelper<>(this, buildContext);
senderBuilderHelper = new SenderImplementationBuilderHelper<>(this, buildContext);
}
/**
* Configures how Ogham will add default values to the {@link Sms} if some
* information is missing.
*
* If sender phone number is missing, a default one can be defined in
* configuration properties.
*
* If recipient phone number is missing, a default one can be defined in
* configuration properties.
*
* <pre>
* <code>
* builder
* .autofill()
* .from()
* .defaultValue().properties("${ogham.sms.from.default-value}").and()
* .and()
* .to()
* .defaultValue().properties("${ogham.sms.to.default-value}").and()
* .and()
* .and()
* </code>
* </pre>
*
* @return the builder to configure autofilling of SMS
*/
public AutofillSmsBuilder autofill() {
if (autofillBuilder == null) {
autofillBuilder = new AutofillSmsBuilder(this, buildContext);
}
return autofillBuilder;
}
/**
* Configures the phone number conversions (from a {@link PhoneNumber} to an
* {@link AddressedPhoneNumber}).
*
* The {@link PhoneNumber} is used by the developer to provide a simple
* phone number without knowing how phone number works (no need to handle
* formats, addressing, countries...). The {@link AddressedPhoneNumber} is
* used by Ogham implementations to have a phone number that is usable by a
* technical system.
*
* For example:
*
* <pre>
* <code>
* builder
* .numbers()
* .from()
* .format()
* .alphanumericCode().properties("${ogham.sms.from.alphanumeric-code-format.enable}").defaultValue(true).and()
* .shortCode().properties("${ogham.sms.from.short-code-format.enable}").defaultValue(true).and()
* .internationalNumber().properties("${ogham.sms.from.international-format.enable}").defaultValue(true).and()
* .and()
* .and()
* .to()
* .format()
* .internationalNumber().properties("${ogham.sms.to.international-format.enable}").defaultValue(true);
* </code>
* </pre>
*
* @return the builder to configure phone number formats
*/
public PhoneNumbersBuilder numbers() {
if (phoneNumbersBuilder == null) {
phoneNumbersBuilder = new PhoneNumbersBuilder(this, buildContext);
}
return phoneNumbersBuilder;
}
/**
* Registers and configures a {@link TemplateParser} through a dedicated
* builder.
*
* For example:
*
* <pre>
* .register(ThymeleafSmsBuilder.class)
* .detector(new ThymeleafEngineDetector());
* </pre>
*
* <p>
* Your {@link Builder} may implement {@link VariantBuilder} to handle
* template {@link Variant}s (used for {@link MultiTemplateContent} that
* provide a single path to templates with different extensions for
* example).
* </p>
*
* <p>
* Your {@link Builder} may also implement {@link DetectorBuilder} in order
* to indicate which kind of templates your {@link TemplateParser} is able
* to parse. If your template parse is able to parse any template file you
* are using, you may not need to implement {@link DetectorBuilder}.
* </p>
*
* <p>
* In order to be able to keep chaining, you builder instance may provide a
* constructor with two arguments:
* <ul>
* <li>The type of the parent builder ({@code <P>})</li>
* <li>The {@link BuildContext} instance</li>
* </ul>
* If you don't care about chaining, just provide a default constructor.
*
* @param builderClass
* the builder class to instantiate
* @param <T>
* the type of the builder
* @return the builder to configure the implementation
*/
public <T extends Builder<? extends TemplateParser>> T template(Class<T> builderClass) {
return templateBuilderHelper.register(builderClass);
}
/**
* Registers a custom message sender implementation.
*
* <p>
* If your custom implementation is annotated by one or several of:
* <ul>
* <li>{@link RequiredClass}</li>
* <li>{@link RequiredProperty}</li>
* <li>{@link RequiredClasses}</li>
* <li>{@link RequiredProperties}</li>
* </ul>
* Then if condition evaluation returns true, your implementation will be
* used. If you provide several annotations, your implementation will be
* used only if all conditions are met (and operator).
*
* <p>
* If your custom implementation implements {@link ActivableAtRuntime}, and
* the provided condition evaluation returns true, then your implementation
* will be used.
*
* See {@link MessageConditions} to build your condition.
* </p>
*
* <p>
* If neither annotations nor implementation of {@link ActivableAtRuntime}
* is used, then your custom implementation will be always used. All other
* implementations (even standard ones) will never be used.
* </p>
*
* @param sender
* the sender to register
* @return this instance for fluent chaining
*/
public SmsBuilder customSender(MessageSender sender) {
senderBuilderHelper.customSender(sender);
return this;
}
/**
* Registers and configures sender through a dedicated builder.
*
* For example:
*
* <pre>
* .sender(CloudhopperBuilder.class)
* .host("localhost");
* </pre>
*
* <p>
* If your custom builder is annotated by one or several of:
* <ul>
* <li>{@link RequiredClass}</li>
* <li>{@link RequiredProperty}</li>
* <li>{@link RequiredClasses}</li>
* <li>{@link RequiredProperties}</li>
* </ul>
* Then if condition evaluation returns true, your built implementation will
* be used. If you provide several annotations, your built implementation
* will be used only if all conditions are met (and operator).
*
* <p>
* If your custom builder implements {@link ActivableAtRuntime}, and the
* provided condition evaluation returns true, then your built
* implementation will be used.
*
* See {@link MessageConditions} to build your condition.
* </p>
*
* <p>
* If neither annotations nor implementation of {@link ActivableAtRuntime}
* is used, then your built implementation will be always used. All other
* implementations (even standard ones) will never be used.
* </p>
*
* <p>
* In order to be able to keep chaining, you builder instance may provide a
* constructor with one argument with the type of the parent builder
* ({@link SmsBuilder}). If you don't care about chaining, just provide a
* default constructor.
* </p>
*
* <p>
* Your builder may return {@code null} when calling
* {@link Builder#build()}. In this case it means that your implementation
* can't be used due to current environment. Your implementation is then not
* registered.
* </p>
*
* @param builderClass
* the builder class to instantiate
* @param <T>
* the type of the builder
* @return the builder to configure the implementation
*/
public <T extends Builder<? extends MessageSender>> T sender(Class<T> builderClass) {
return senderBuilderHelper.register(builderClass);
}
/**
* Configure automatic retry if message couldn't be sent.
*
*
* For example:
*
* <pre>
* .autoRetry()
* .fixedDelay()
* .maxRetries().properties("${ogham.sms.send-retry.max-attempts}").and()
* .delay().properties("${ogham.sms.send-retry.delay-between-attempts}")
* </pre>
*
*
* <p>
* This builder lets you configure:
* <ul>
* <li>The retry strategy:
* <ul>
* <li>{@link FixedDelayRetry}: wait for a fixed delay after the last
* failure</li>
* <li>{@link FixedIntervalRetry}: wait for a fixed delay between executions
* (do not wait for the end of the action)</li>
* <li>{@link ExponentialDelayRetry}: start with a delay, the next delay
* will be doubled on so on</li>
* <li>{@link PerExecutionDelayRetry}: provide a custom delay for each
* execution</li>
* </ul>
* </li>
* <li>The {@link RetryExecutor} implementation</li>
* <li>The {@link Awaiter} implementation</li>
* <li>The {@link Condition} used to determine if the raised error should
* trigger a retry or not</li>
* </ul>
*
* @return the builder to configure retry management
*/
public RetryBuilder<SmsBuilder> autoRetry() {
if (retryBuilder == null) {
retryBuilder = new RetryBuilder<>(this, buildContext);
}
return retryBuilder;
}
@Override
public ConditionalSender build() {
SmsSender smsSender = buildContext.register(new SmsSender());
ConditionalSender sender = smsSender;
senderBuilderHelper.addSenders(smsSender);
if (templateBuilderHelper.hasRegisteredTemplates()) {
ContentTranslator translator = buildContentTranslator();
LOG.debug("Content translation enabled {}", translator);
sender = buildContext.register(new ContentTranslatorSender(translator, sender));
}
if (phoneNumbersBuilder != null) {
PhoneNumberTranslatorPair pair = phoneNumbersBuilder.build();
sender = buildContext.register(new PhoneNumberTranslatorSender(pair.getSender(), pair.getRecipient(), sender));
}
if (autofillBuilder != null) {
MessageFiller messageFiller = autofillBuilder.build();
LOG.debug("Automatic filling of message enabled {}", messageFiller);
sender = buildContext.register(new FillerSender(messageFiller, sender));
}
RetryExecutor retryExecutor = BuilderUtils.build(retryBuilder);
if (retryExecutor != null) {
LOG.debug("Automatic retry of message sending enabled {}", retryExecutor);
sender = buildContext.register(new AutoRetrySender(sender, retryExecutor));
}
return sender;
}
private ContentTranslator buildContentTranslator() {
EveryContentTranslator translator = buildContext.register(new EveryContentTranslator());
addTemplateTranslator(translator);
return translator;
}
private void addTemplateTranslator(EveryContentTranslator translator) {
if (!templateBuilderHelper.hasRegisteredTemplates()) {
return;
}
TemplateParser templateParser = templateBuilderHelper.buildTemplateParser();
LOG.debug("Registering content translator that parses templates using {}", templateParser);
translator.addTranslator(buildContext.register(new TemplateContentTranslator(templateParser)));
}
}