DefaultMessagingConfigurer.java

package fr.sii.ogham.core.builder.configurer;

import static fr.sii.ogham.core.CoreConstants.CLASSPATH_LOOKUPS;
import static fr.sii.ogham.core.CoreConstants.DEFAULT_MESSAGING_CONFIGURER_PRIORITY;
import static fr.sii.ogham.core.CoreConstants.FILE_LOOKUPS;
import static fr.sii.ogham.core.CoreConstants.STRING_LOOKUPS;
import static fr.sii.ogham.core.builder.configuration.MayOverride.overrideIfNotSet;
import static fr.sii.ogham.core.builder.configurer.SendMessageRetryablePredicates.canResendMessage;
import static java.util.Arrays.asList;

import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.apache.tika.Tika;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.builder.env.EnvironmentBuilder;
import fr.sii.ogham.core.builder.mimetype.MimetypeDetectionBuilder;
import fr.sii.ogham.core.builder.mimetype.TikaBuilder;
import fr.sii.ogham.core.builder.resolution.ResourceResolutionBuilder;
import fr.sii.ogham.core.convert.DefaultConverter;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.builder.AutofillDefaultEmailAddressBuilder;
import fr.sii.ogham.email.builder.AutofillSubjectBuilder;
import fr.sii.ogham.email.builder.CssInliningBuilder;
import fr.sii.ogham.email.builder.EmailBuilder;
import fr.sii.ogham.email.builder.ImageInliningBuilder;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.sms.builder.AutofillSmsBuilder;
import fr.sii.ogham.sms.builder.RecipientNumberFormatBuilder;
import fr.sii.ogham.sms.builder.SenderNumberFormatBuilder;
import fr.sii.ogham.sms.builder.SmsBuilder;
import fr.sii.ogham.sms.message.Sms;

/**
 * Default configurer that is automatically applied every time a
 * {@link MessagingBuilder} instance is created through
 * {@link MessagingBuilder#standard()} or {@link MessagingBuilder#minimal()}.
 * 
 * <p>
 * The configurer has a priority of 100000 in order to be applied before all
 * other configurers.
 * </p>
 * 
 * This configurer applies general configuration (see
 * {@link EnvironmentBuilder}):
 * <ul>
 * <li>The {@link MessagingService} will catch all uncaught exception (even
 * runtime) in order to wrap them in a {@link MessagingException}.</li>
 * <li>General environment configuration:
 * <ul>
 * <li>System properties with</li>
 * <li>Uses the {@link DefaultConverter}</li>
 * <li><strong>Environment will be inherited by sub-builders by
 * default.</strong></li>
 * </ul>
 * </li>
 * <li>Resource resolution configuration (see
 * {@link ResourceResolutionBuilder}):
 * <ul>
 * <li>Lookup prefixes for classpath: "classpath:" and no lookup prefix (if no
 * prefix is defined, then classpath is used).
 * <li>Lookup prefix for file: "file:".
 * <li>Lookup prefixes for string: "string:", "s:.
 * <li><strong>Resource resolution will be by default inherited by
 * sub-builders</strong></li>
 * </ul>
 * </li>
 * <li>Configure common email behaviors:
 * <ul>
 * <li>Configure resource resolution for attachments, css inlining and image
 * inlining using same resource resolution configuration as the general one (see
 * {@link ResourceResolutionBuilder})</li>
 * <li>Configure mimetype detection for attachments and image inlining (see
 * {@link MimetypeDetectionBuilder})</li>
 * <li>Automatically fill {@link Email} messages with subject either from title
 * tag of html template, from first line starting with "Subject:" of text
 * template or using {@code ogham.email.subject} property if defined (see
 * {@link AutofillSubjectBuilder})</li>
 * <li>Autofill {@link Email} messages with sender address if one of the
 * property {@code ogham.email.from.default-value} or {@code mail.smtp.from} is
 * defined (see {@link AutofillDefaultEmailAddressBuilder})</li>
 * <li>Autofill {@link Email} messages with recipient address (to) if the
 * property {@code ogham.email.to.default-value} is defined (see
 * {@link AutofillDefaultEmailAddressBuilder})</li>
 * <li>Autofill {@link Email} messages with recipient address (cc) if the
 * property {@code ogham.email.cc.default-value} is defined (see
 * {@link AutofillDefaultEmailAddressBuilder})</li>
 * <li>Autofill {@link Email} messages with recipient address (bcc) if the
 * property {@code ogham.email.bcc.default-value} is defined (see
 * {@link AutofillDefaultEmailAddressBuilder})</li>
 * <li>Automatically inline CSS styles in the HTML templates (see
 * {@link CssInliningBuilder})</li>
 * <li>Automatically inline images in the email either by attaching them or by
 * converting them into base64 (see {@link ImageInliningBuilder})</li>
 * </ul>
 * </li>
 * <li>Configure common SMS behaviors:
 * <ul>
 * <li>Autofill {@link Sms} messages with sender phone number if the property
 * {@code ogham.sms.from.default-value} is defined (see
 * {@link AutofillSmsBuilder})</li>
 * <li>Autofill {@link Sms} messages with recipient phone number if the property
 * {@code ogham.sms.to.default-value} is defined (see
 * {@link AutofillSmsBuilder})</li>
 * <li>Configure phone number formats (see {@link SenderNumberFormatBuilder} and
 * {@link RecipientNumberFormatBuilder})</li>
 * </ul>
 * </li>
 * <li>Mimetype detection configuration:
 * <ul>
 * <li>Uses {@link Tika} to detect mimetype (see {@link TikaBuilder})</li>
 * <li>Uses property {@code ogham.mimetype.default-mimetype} if Tika has not
 * detected the mimetype (see {@link MimetypeDetectionBuilder})</li>
 * <li>Uses {@code application/octet-stream} if neither Tika has detected
 * mimetype nor default property value has been set</li>
 * </ul>
 * </ul>
 * 
 * @author Aurélien Baudet
 *
 */
@ConfigurerFor(targetedBuilder = { "minimal", "standard" }, priority = DEFAULT_MESSAGING_CONFIGURER_PRIORITY)
public class DefaultMessagingConfigurer extends MessagingConfigurerAdapter {
	private static final Logger LOG = LoggerFactory.getLogger(DefaultMessagingConfigurer.class);
	private static final Pattern LOCATIONS_SEPARATOR = Pattern.compile(",\\s*");
	private static final Pattern PROFILES_SEPARATOR = Pattern.compile(",\\s*");
	
	private final Supplier<List<String>> profilesSupplier;
	private final Supplier<List<String>> locationsSupplier;
	
	public DefaultMessagingConfigurer() {
		this(() -> asList(PROFILES_SEPARATOR.split(System.getProperty("ogham.profiles.active", "default"))),
			() -> asList(LOCATIONS_SEPARATOR.split(System.getProperty("ogham.config.location", ""))));
	}
	
	public DefaultMessagingConfigurer(Supplier<List<String>> profilesSupplier, Supplier<List<String>> locationsSupplier) {
		super();
		this.profilesSupplier = profilesSupplier;
		this.locationsSupplier = locationsSupplier;
	}

	@Override
	public void configure(MessagingBuilder builder) {
		LOG.debug("[{}] apply configuration", this);
		super.configure(builder);
		builder.wrapUncaught().properties("${ogham.wrap-uncaught-exceptions.enable}").defaultValue(overrideIfNotSet(true));
	}

	@Override
	public void configure(EnvironmentBuilder<?> builder) {
		// @formatter:off
		builder
			.systemProperties()
			.converter()
				.defaultConverter(overrideIfNotSet(new DefaultConverter()));
		// @formatter:on
		List<String> profiles = profilesSupplier.get();
		List<String> locations = locationsSupplier.get();
		addUserDefinedConfigLocationsForProfiles(builder, locations, profiles);
		addUserDefinedConfigLocations(builder, locations);
		addDefaultConfigLocationsForProfiles(builder, profiles);
		addDefaultConfigLocations(builder);
	}

	@Override
	public void configure(ResourceResolutionBuilder<?> builder) {
		// @formatter:off
		builder
			.string()
				.lookup(STRING_LOOKUPS)
				.and()
			.file()
				.lookup(FILE_LOOKUPS)
				.and()
			.classpath()
				.lookup(CLASSPATH_LOOKUPS);
		// @formatter:on
	}

	@Override
	public void configure(EmailBuilder builder) {
		// configure resource resolution for attachments, css and images
		configure(builder.attachments());
		configure(builder.css().inline());
		configure(builder.images().inline());
		// configure mimetype detection for images
		configureImageInliningMimetype(builder.images().inline().mimetype());

		// @formatter:off
		builder
			.autofill()
				.subject()
					.defaultValue().properties("${ogham.email.subject.default-value}").and()
					.htmlTitle().properties("${ogham.email.subject.extract-html-title.enable}").defaultValue(overrideIfNotSet(true)).and()
					.text().properties("${ogham.email.subject.extract-from-text.first-line-prefix}").defaultValue(overrideIfNotSet("Subject:")).and()
					.and()
				.from()
					.defaultValue().properties("${ogham.email.from.default-value}", "${mail.smtp.from}", "${mail.from}").and()
					.and()
				.to()
					.defaultValue().properties("${ogham.email.to.default-value}").and()
					.and()
				.cc()
					.defaultValue().properties("${ogham.email.cc.default-value}").and()
					.and()
				.bcc()
					.defaultValue().properties("${ogham.email.bcc.default-value}").and()
					.and()
				.and()
			.css()
				.inline()
					.jsoup()
					.and()
				.and()
			.images()
				.inline()
					.attach()
						.cid()
							.sequential()
							.and()
						.and()
					.base64().and()
					.and()
				.and()
			.failIfMissingVariant().defaultValue(overrideIfNotSet(true)).and()
			.listPossiblePaths().defaultValue(overrideIfNotSet(true)).and()
			.autoRetry()
				.fixedDelay()
					.maxRetries().properties("${ogham.email.send-retry.max-attempts}").and()
					.delay().properties("${ogham.email.send-retry.delay-between-attempts}").and()
					.and()
				.exponentialDelay()
					.maxRetries().properties("${ogham.email.send-retry.max-attempts}").and()
					.initialDelay().properties("${ogham.email.send-retry.exponential-initial-delay}").and()
					.and()
				.perExecutionDelay()
					.maxRetries().properties("${ogham.email.send-retry.max-attempts}").and()
					.delays().properties("${ogham.email.send-retry.per-execution-delays}").and()
					.and()
				.fixedInterval()
					.maxRetries().properties("${ogham.email.send-retry.max-attempts}").and()
					.interval().properties("${ogham.email.send-retry.execution-interval}").and()
					.and()
				.retryable(canResendMessage());
		// @formatter:on
	}

	@Override
	public void configure(SmsBuilder builder) {
		// @formatter:off
		builder
			.autofill()
				.from()
					.defaultValue().properties("${ogham.sms.from.default-value}").and()
					.and()
				.to()
					.defaultValue().properties("${ogham.sms.to.default-value}").and()
					.and()
				.and()
			.numbers()
				.from()
					.format()
						.alphanumericCode().properties("${ogham.sms.from.alphanumeric-code-format.enable}").defaultValue(overrideIfNotSet(true)).and()
						.shortCode().properties("${ogham.sms.from.short-code-format.enable}").defaultValue(overrideIfNotSet(true)).and()
						.internationalNumber().properties("${ogham.sms.from.international-format.enable}").defaultValue(overrideIfNotSet(true)).and()
						.and()
					.and()
				.to()
					.format()
						.internationalNumber().properties("${ogham.sms.to.international-format.enable}").defaultValue(overrideIfNotSet(true)).and()
						.and()
					.and()
				.and()
			.autoRetry()
				.fixedDelay()
					.maxRetries().properties("${ogham.sms.send-retry.max-attempts}").and()
					.delay().properties("${ogham.sms.send-retry.delay-between-attempts}").and()
					.and()
				.exponentialDelay()
					.maxRetries().properties("${ogham.sms.send-retry.max-attempts}").and()
					.initialDelay().properties("${ogham.sms.send-retry.exponential-initial-delay}").and()
					.and()
				.perExecutionDelay()
					.maxRetries().properties("${ogham.sms.send-retry.max-attempts}").and()
					.delays().properties("${ogham.sms.send-retry.per-execution-delays}").and()
					.and()
				.fixedInterval()
					.maxRetries().properties("${ogham.sms.send-retry.max-attempts}").and()
					.interval().properties("${ogham.sms.send-retry.execution-interval}").and()
					.and()
				.retryable(canResendMessage());
		// @formatter:on
	}

	@Override
	public void configure(MimetypeDetectionBuilder<?> builder) {
		// @formatter:off
		builder
			.tika()
				.instance(new Tika())
				.failIfOctetStream().properties("${ogham.mimetype.tika.fail-if-octet-stream}").defaultValue(overrideIfNotSet(true)).and()
				.and()
			.defaultMimetype().properties("${ogham.mimetype.default-mimetype}").defaultValue(overrideIfNotSet("application/octet-stream"));
		// @formatter:on
	}

	protected void configureImageInliningMimetype(MimetypeDetectionBuilder<?> builder) {
		// @formatter:off
		builder
			.tika()
				.instance(new Tika())
				.failIfOctetStream().defaultValue(overrideIfNotSet(true)).and()
				.and()
			.allowed().properties("${ogham.email.image-inlining.mimetype.allowed-mimetypes}").defaultValue(overrideIfNotSet(new String[] { "image/*" }));
		// @formatter:on
	}
	

	protected void addDefaultConfigLocationsForProfiles(EnvironmentBuilder<?> builder, List<String> profiles) {
		for (String profile : profiles) {
			addConfigLocations(builder, "file:config", profile);
			addConfigLocations(builder, "classpath:config", profile);
		}
	}

	protected void addDefaultConfigLocations(EnvironmentBuilder<?> builder) {
		addConfigLocations(builder, "file:config");
		addConfigLocations(builder, "classpath:config");
	}

	protected void addUserDefinedConfigLocations(EnvironmentBuilder<?> builder, List<String> locations) {
		for (String location : locations) {
			addConfigLocations(builder, location);
		}
	}
	
	protected void addUserDefinedConfigLocationsForProfiles(EnvironmentBuilder<?> builder, List<String> locations, List<String> profiles) {
		for (String location : locations) {
			for (String profile : profiles) {
				addConfigLocations(builder, location, profile);
			}
		}
	}
	
	protected void addConfigLocations(EnvironmentBuilder<?> builder, String location) {
		addConfigLocations(builder, location, null);
	}
	
	protected void addConfigLocations(EnvironmentBuilder<?> builder, String location, String profile) {
		if (location.isEmpty()) {
			return;
		}
		String suffix = profile == null ? "" : ("-" + profile);
		if (suffix.isEmpty()) {
			builder.properties("?"+location);
		}
		builder.properties("?"+location+"/ogham"+suffix+".properties");
		builder.properties("?"+location+"/application"+suffix+".properties");
	}
}