SendGridV4Builder.java

package fr.sii.ogham.email.sendgrid.v4.builder.sendgrid;

import java.net.URL;

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sendgrid.Client;
import com.sendgrid.SendGrid;

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.message.content.MayHaveStringContent;
import fr.sii.ogham.core.message.content.MultiContent;
import fr.sii.ogham.core.mimetype.MimeTypeProvider;
import fr.sii.ogham.email.builder.EmailBuilder;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.email.message.content.ContentWithAttachments;
import fr.sii.ogham.email.sendgrid.builder.AbstractSendGridBuilder;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.SendGridV4Sender;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.client.CustomizableUrlClient;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.client.DelegateSendGridClient;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.client.SendGridClient;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.client.SendGridInterceptor;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.compat.CompatUtil;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.handler.ContentWithAttachmentsHandler;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.handler.MultiContentHandler;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.handler.PriorizedContentHandler;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.handler.StringContentHandler;

/**
 * Configures how SendGrid implementation will send {@link Email}s.
 * 
 * This implementation uses SendGrid HTTP API.
 * 
 * <p>
 * To send {@link Email} using SendGrid, you need to register this builder into
 * a {@link MessagingBuilder} like this:
 * 
 * <pre>
 * <code>
 * MessagingBuilder msgBuilder = ...
 * msgBuilder.email()
 *    .sender(SendGridV4Builder.class)    // registers the builder and accesses to that builder for configuring it
 * </code>
 * </pre>
 * 
 * Once the builder is registered, sending email through SendGrid requires
 * either an API key or a username/password pair. You can define it using:
 * 
 * <pre>
 * <code>
 * msgBuilder.email()
 *    .sender(SendGridV4Builder.class)    // registers the builder and accesses to that builder for configuring it
 *       .apiKey("foo")
 * </code>
 * </pre>
 * 
 * Or you can also use property keys (using interpolation):
 * 
 * <pre>
 * <code>
 * msgBuilder
 * .environment()
 *    .properties()
 *       .set("custom.property.for.api-key", "foo")
 *       .and()
 *    .and()
 * .email()
 *    .sender(SendGridV4Builder.class)    // registers the builder and accesses to that builder for configuring it
 *       .apiKey("${custom.property.for.api-key}")
 * </code>
 * </pre>
 * 
 * <p>
 * Finally, Ogham will transform general {@link Email} object into
 * {@link SendGrid}.Email object. This transformation will fit almost all use
 * cases but you may need to customize a part of the SendGrid message. Instead
 * of doing again the same work Ogham does, this builder allows you to intercept
 * the message to modify it just before sending it:
 * 
 * <pre>
 * <code>
 * .sender(SendGridV4Builder.class)
 *    .intercept(new MyCustomInterceptor())
 * </code>
 * </pre>
 * 
 * See {@link SendGridInterceptor} for more information.
 * 
 * @author Aurélien Baudet
 *
 */
public class SendGridV4Builder extends AbstractSendGridBuilder<SendGridV4Builder, EmailBuilder> {
	private static final Logger LOG = LoggerFactory.getLogger(SendGridV4Builder.class);

	private SendGridClient client;
	private SendGridInterceptor interceptor;
	private Client clientHelper;
	private final ConfigurationValueBuilderHelper<SendGridV4Builder, Boolean> unitTestingValueBuilder;

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

	/**
	 * Constructor that is called when using Ogham builder:
	 * 
	 * <pre>
	 * MessagingBuilder msgBuilder = ...
	 * msgBuilder
	 * .email()
	 *    .sender(SendGridV4Builder.class)
	 * </pre>
	 * 
	 * @param parent
	 *            the parent builder instance for fluent chaining
	 * @param buildContext
	 *            for registering instances and property evaluation
	 */
	public SendGridV4Builder(EmailBuilder parent, BuildContext buildContext) {
		super(SendGridV4Builder.class, parent, buildContext);
		unitTestingValueBuilder = buildContext.newConfigurationValueBuilder(this, Boolean.class);
	}

	/**
	 * @deprecated SendGrid v4 doesn't use username/password anymore. You must
	 *             use an {@link #apiKey(String)}.
	 */
	@Deprecated
	@Override
	public SendGridV4Builder username(String username) {
		LOG.warn("username and password are no more available with SendGrid v4");
		return this;
	}

	/**
	 * @deprecated SendGrid v4 doesn't use username/password anymore. You must
	 *             use an {@link #apiKey(String)}.
	 */
	@Deprecated
	@Override
	public ConfigurationValueBuilder<SendGridV4Builder, String> username() {
		LOG.warn("username and password are no more available with SendGrid v4");
		return buildContext.newConfigurationValueBuilder(this, String.class);
	}

	/**
	 * @deprecated SendGrid v4 doesn't use username/password anymore. You must
	 *             use an {@link #apiKey(String)}.
	 */
	@Deprecated
	@Override
	public SendGridV4Builder password(String password) {
		LOG.warn("username and password are no more available with SendGrid v4");
		return this;
	}

	/**
	 * @deprecated SendGrid v4 doesn't use username/password anymore. You must
	 *             use an {@link #apiKey(String)}.
	 */
	@Deprecated
	@Override
	public ConfigurationValueBuilder<SendGridV4Builder, String> password() {
		LOG.warn("username and password are no more available with SendGrid v4");
		return buildContext.newConfigurationValueBuilder(this, String.class);
	}

	/**
	 * SendGrid allows to call API for unit tests. Set this to true if you are
	 * unit testing.
	 * 
	 * <p>
	 * The value set using this method takes precedence over any property and
	 * default value configured using {@link #unitTesting()}.
	 * 
	 * <pre>
	 * .unitTesting(true)
	 * .unitTesting()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * <pre>
	 * .unitTesting(true)
	 * .unitTesting()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * In both cases, {@code unitTesting(true)} 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 unitTesting
	 *            true to use SendGrid in unit testing mode
	 * @return this instance for fluent chaining
	 */
	public SendGridV4Builder unitTesting(Boolean unitTesting) {
		unitTestingValueBuilder.setValue(unitTesting);
		return this;
	}

	/**
	 * SendGrid allows to call API for unit tests. Set this to true if you are
	 * unit testing.
	 * 
	 * <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>
	 * .unitTesting()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * <p>
	 * Non-null value set using {@link #unitTesting(Boolean)} takes precedence
	 * over property values and default value.
	 * 
	 * <pre>
	 * .unitTesting(true)
	 * .unitTesting()
	 *   .properties("${custom.property.high-priority}", "${custom.property.low-priority}")
	 *   .defaultValue(false)
	 * </pre>
	 * 
	 * The value {@code true} 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<SendGridV4Builder, Boolean> unitTesting() {
		return unitTestingValueBuilder;
	}

	/**
	 * By default, SendGrid uses a {@link Client} instance as an helper to
	 * perform HTTP requests. You may want to use custom client configuration
	 * such as providing custom protocol and port:
	 * 
	 * <pre>
	 * .client(new CustomizableUrlClient(false, "http", 8080))
	 * </pre>
	 * 
	 * NOTE: if you provide a custom {@link Client}, the
	 * {@link #unitTesting(Boolean)} or
	 * {@link #httpClient(org.apache.http.impl.client.CloseableHttpClient)}
	 * configurations are not used. You have to handle it manually.
	 * 
	 * @param clientHelper
	 *            the custom client used to call SendGrid HTTP API
	 * @return this instance for fluent chaining
	 */
	public SendGridV4Builder clientHelper(Client clientHelper) {
		this.clientHelper = clientHelper;
		return this;
	}

	/**
	 * By default, calling SendGrid HTTP API is done through the default
	 * {@link SendGrid} implementation. If you want to use another client
	 * implementation (creating your custom HTTP API caller for example), you
	 * can implement the {@link SendGridClient} interface and provide it:
	 * 
	 * <pre>
	 * .client(new MyCustomHttpApiCaller())
	 * </pre>
	 * 
	 * NOTE: if you provide your custom implementation, any defined properties
	 * and values using {@link #apiKey(String)}, {@link #username(String)} or
	 * {@link #password(String)} won't be used at all. You then have to handle
	 * it by yourself.
	 * 
	 * @param client
	 *            the custom client used to call SendGrid HTTP API
	 * @return this instance for fluent chaining
	 */
	public SendGridV4Builder client(SendGridClient client) {
		this.client = client;
		return this;
	}

	/**
	 * By default, calling SendGrid HTTP API is done through the default
	 * {@link SendGrid} implementation that uses default {@link HttpClient}
	 * (calling {@code HttpClientBuilder.create().build()}). If you want to use
	 * another HTTP client implementation, you can extend the
	 * {@link CloseableHttpClient} class and provide it:
	 * 
	 * <pre>
	 * .client(new MyCustomHttpClient())
	 * </pre>
	 * 
	 * NOTE: if you provide your custom implementation, any defined properties
	 * and values using {@link #unitTesting(Boolean)} won't be used at all. You
	 * then have to handle it by yourself.
	 * 
	 * @param httpClient
	 *            the custom implementation of {@link HttpClient} used to call
	 *            SendGrid HTTP API. SendGrid requires a
	 *            {@link CloseableHttpClient}.
	 * @return this instance for fluent chaining
	 */
	@SuppressWarnings("squid:S1185")
	@Override
	public SendGridV4Builder httpClient(CloseableHttpClient httpClient) {
		return super.httpClient(httpClient);
	}

	/**
	 * Ogham will transform general {@link Email} object into
	 * {@link SendGrid}.Email objects. This transformation will fit almost all
	 * use cases but you may need to customize a part of the SendGrid message.
	 * Instead of doing again the same work Ogham does, this builder allows you
	 * to intercept the message to modify it just before sending it:
	 * 
	 * <pre>
	 * .sender(SendGridV4Builder.class)
	 *    .intercept(new MyCustomInterceptor())
	 * </pre>
	 * 
	 * See {@link SendGridInterceptor} for more information.
	 * 
	 * @param interceptor
	 *            the custom interceptor used to modify {@link SendGrid}.Email
	 * @return this instance for fluent chaining
	 */
	public SendGridV4Builder intercept(SendGridInterceptor interceptor) {
		this.interceptor = interceptor;
		return this;
	}

	@Override
	public SendGridV4Sender build() {
		String apiKey = apiKeyValueBuilder.getValue();
		boolean test = unitTestingValueBuilder.getValue(false);
		URL url = urlValueBuilder.getValue();
		SendGridClient builtClient = buildClient(apiKey, buildClientHelper(clientHelper, httpClient, test, url), url);
		if (builtClient == null) {
			return null;
		}
		LOG.info("Sending email using SendGrid API is registered");
		if (client == null) {
			LOG.debug("SendGrid account: apiKey={}, test={}", apiKey, test);
		} else {
			LOG.debug("SendGrid instance provided so apiKey and unitTesting properties are not used");
		}
		return buildContext.register(new SendGridV4Sender(builtClient, buildContentHandler(), buildMimetypeProvider(), CompatUtil.getDefaultCompatFactory(), interceptor));
	}

	private Client buildClientHelper(Client clientHelper, CloseableHttpClient httpClient, boolean test, URL url) {
		// custom implementation
		if (clientHelper != null) {
			return clientHelper;
		}
		// case where custom URL is set.
		// SendGrid Client doesn't support neither custom port nor custom
		// protocol
		if (url != null && httpClient != null) {
			return buildContext.register(new CustomizableUrlClient(httpClient, url.getProtocol(), url.getPort()));
		}
		if (url != null) {
			return buildContext.register(new CustomizableUrlClient(test, url.getProtocol(), url.getPort()));
		}
		// custom http client
		if (httpClient != null) {
			return buildContext.register(new Client(httpClient));
		}
		// test client (just to allow http instead of https)
		if (test) {
			return buildContext.register(new Client(true));
		}
		// use default Client implementation created directly by SendGrid
		return null;
	}

	private SendGridClient buildClient(String apiKey, Client client, URL url) {
		if (this.client != null) {
			return this.client;
		}
		if (apiKey != null) {
			return buildContext.register(new DelegateSendGridClient(buildSendGrid(apiKey, client, url)));
		}
		return null;
	}

	private SendGrid buildSendGrid(String apiKey, Client client, URL url) {
		SendGrid sendGrid = newSendGrid(apiKey, client);
		if (url != null) {
			sendGrid.setHost(url.getHost());
		}
		return sendGrid;
	}

	private static SendGrid newSendGrid(String apiKey, Client client) {
		if (client != null) {
			return new SendGrid(apiKey, client);
		}
		return new SendGrid(apiKey);
	}

	private PriorizedContentHandler buildContentHandler() {
		MimeTypeProvider mimetypeProvider = buildMimetypeProvider();
		PriorizedContentHandler contentHandler = buildContext.register(new PriorizedContentHandler());
		contentHandler.register(MultiContent.class, buildContext.register(new MultiContentHandler(contentHandler)));
		contentHandler.register(ContentWithAttachments.class, buildContext.register(new ContentWithAttachmentsHandler(contentHandler)));
		contentHandler.register(MayHaveStringContent.class, buildContext.register(new StringContentHandler(mimetypeProvider)));
		return contentHandler;
	}

	private MimeTypeProvider buildMimetypeProvider() {
		if (mimetypeBuilder == null) {
			return null;
		}
		return mimetypeBuilder.build();
	}
}