MailerProxy.java
/***************************************************************************
Copyright 2015 Emily Estes
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***************************************************************************/
package net.metanotion.email;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.metanotion.util.NamedParameterMapper;
/** A common pattern in the web application components are interfaces to define the abstract notion of transactional
emails to be sent up on certain events. Two examples are {@link net.metanotion.formsauth.AuthMailer} and
{@link net.metanotion.multitenant.adminapp.AdminMailer}. The idea is that rather than the text of the email being
embedded directly in the component, the application can provide a mechanism like the templated emails of the
{@link net.metanotion.email.MailMerge} class to generate the emails on demand. This class implements one strategy based
on the mail merge component in this package and another strategy which just logs the parameters of the message using
the logging framework. Java reflection is used to generate an implementation from the interface directly. */
public final class MailerProxy implements InvocationHandler {
/** This is a marker interface. An interface can provide a class of "constants"/a struct. The values of the public
fields of this class will be used to generate a list of constants added to every email template generation.
@param <I> The interface the constant implementation goes with.
*/
public interface Constants<I> {}
private final Logger logger;
private final Class klazz;
/** Used to queue the message sends asynchronously. */
private final ExecutorService es;
/** The mail merge instance to generate the emails. */
private final MailMerge mm;
/** The email address the messages will be from. */
private final String senderEmail;
/** A map of message types to base URL's for the generated URL's in the templates. */
private final Map<String,String> constantVars;
/** A map of messages types to template URI's to generate the email. */
private final Map<String,String> templates;
/** A map of a list of the names of the parameters to the methods. */
private final Map<String,Iterable<String>> methodVars;
private MailerProxy(final Class klazz,
final ExecutorService es,
final MailMerge mm,
final String senderEmail,
final Map<String, String> constantVars,
final Map<String, String> templates) {
this.klazz = klazz;
this.es = es;
this.mm = mm;
this.senderEmail = senderEmail;
this.constantVars = constantVars;
this.templates = templates;
this.logger = LoggerFactory.getLogger(klazz);
this.methodVars = (new NamedParameterMapper()).read(klazz);
}
@Override public Object invoke(final Object proxy, final Method method, final Object[] args) {
final String methodName = method.getName();
final String template = templates.get(methodName);
final String toEmail = args[0].toString();
final HashMap<String,Object> vars = new HashMap<>();
for(final Map.Entry<String,String> e: constantVars.entrySet()) {
vars.put(e.getKey(), e.getValue());
}
int i = 0;
for(final String parameterName: methodVars.get(methodName)) {
vars.put(parameterName, args[i].toString());
i++;
}
es.submit(new Runnable() {
@Override public void run() {
try {
mm.sendEmail(senderEmail, toEmail, vars, template);
} catch (final Exception e) {
logger.error("email not sent. {}", e);
}
}
});
return null;
}
/** Convert an instance of a struct to a map ccontaining the values of the fields in the struct whose keys are the
names of the fields.
@param struct The instance to convert.
@return A map containing the values of the struct.
*/
public static Map<String, String> classToVars(final Constants struct) {
try {
final HashMap<String, String> vars = new HashMap<>();
for(final Field f: struct.getClass().getDeclaredFields()) {
final int m = f.getModifiers();
if(Modifier.isPublic(m) && !Modifier.isTransient(m)) {
vars.put(f.getName(), f.get(struct).toString());
}
}
return vars;
} catch (final IllegalAccessException iae) {
throw new RuntimeException(iae);
}
}
/** This generates resource names to look up via the provided factory. It takes the method name from the interface
and applies the path prefix "/" to it, and appends ".txt" to the end to generate a filename.
@param klazz The interface to generate names from.
@return A map of method names to filenames of the templates to load for the method.
*/
public static Map<String,String> defaultTemplates(final Class klazz) { return defaultTemplates(klazz, "/"); }
/** This generates resource names to look up via the provided factory. It takes the method name from the interface
and applies a path prefix to it, and appends ".txt" to the end to generate a filename.
@param klazz The interface to generate names from.
@param prefix The path prefix to add to the method names.
@return A map of method names to filenames of the templates to load for the method.
*/
public static Map<String,String> defaultTemplates(final Class klazz, final String prefix) {
final HashMap<String,String> templates = new HashMap<>();
for(final Method m: klazz.getMethods()) {
templates.put(m.getName(), prefix + m.getName() + ".txt");
}
return templates;
}
/** Generate an instance of the mailing interface that generates log messages using the logging interface.
@param <I> The type of the mailer interface.
@param klazz The mailer interface to generate an implementation against.
@return An instance of klazz that logs method calls.
*/
public static <I> I mailLogger(final Class<I> klazz) { return mailLogger(klazz, "***EMAIL: "); }
/** Generate an instance of the mailing interface that generates log messages using the logging interface.
@param <I> The type of the mailer interface.
@param klazz The mailer interface to generate an implementation against.
@param logMessage The string to start each log message with.
@return An instance of klazz that logs method calls.
*/
public static <I> I mailLogger(final Class<I> klazz, final String logMessage) {
final InvocationHandler handler = new InvocationHandler() {
private final Logger logger = LoggerFactory.getLogger(klazz);
@Override public Object invoke(final Object proxy, final Method method, final Object[] args) {
logger.debug("{}{} - {}", logMessage, method.getName(), args);
return null;
}
};
return (I) Proxy.newProxyInstance(klazz.getClassLoader(), new Class[] { klazz }, handler);
}
/** Generate an instance of the mailing interface that merges templates via the mail merge implementation with the
method parameters and queue's the email.
@param <I> The type of the mailer interface.
@param klazz The mailer interface to generate an implementation against.
@param es An executor service to submit messages to for queueing. When a message is submitted, a job is queued in
this service so that message sends are asynchronous. (Note: messages queued in the executor service are not
persisted in any way, so app failure could result in dropped messages.).
@param mm An instance of the MailMerge class to create the actual messages to send.
@param senderEmail The email address of the message sender. (e.g. noreply@example.com)
@param constants The struct to take the constants to merge with every message.
@return An instance of klazz that merges templates with the method parameters.
*/
public static <I> I generate(final Class<I> klazz,
final ExecutorService es,
final MailMerge mm,
final String senderEmail,
final Constants<I> constants) {
return generate(klazz, es, mm, senderEmail, classToVars(constants), defaultTemplates(klazz));
}
/** Generate an instance of the mailing interface that merges templates via the mail merge implementation with the
method parameters and queue's the email.
@param <I> The type of the mailer interface.
@param klazz The mailer interface to generate an implementation against.
@param es An executor service to submit messages to for queueing. When a message is submitted, a job is queued in
this service so that message sends are asynchronous. (Note: messages queued in the executor service are not
persisted in any way, so app failure could result in dropped messages.).
@param mm An instance of the MailMerge class to create the actual messages to send.
@param senderEmail The email address of the message sender. (e.g. noreply@example.com)
@param constantVars A map of keys and values to merge with every message.
@return An instance of klazz that merges templates with the method parameters.
*/
public static <I> I generate(final Class<I> klazz,
final ExecutorService es,
final MailMerge mm,
final String senderEmail,
final Map<String, String> constantVars) {
return generate(klazz, es, mm, senderEmail, constantVars, defaultTemplates(klazz));
}
/** Generate an instance of the mailing interface that merges templates via the mail merge implementation with the
method parameters and queue's the email.
@param <I> The type of the mailer interface.
@param klazz The mailer interface to generate an implementation against.
@param es An executor service to submit messages to for queueing. When a message is submitted, a job is queued in
this service so that message sends are asynchronous. (Note: messages queued in the executor service are not
persisted in any way, so app failure could result in dropped messages.).
@param mm An instance of the MailMerge class to create the actual messages to send.
@param senderEmail The email address of the message sender. (e.g. noreply@example.com)
@param constantVars A map of keys and values to merge with every message.
@param templates A map of method names to resource names of the templates to use when merging messages.
@return An instance of klazz that merges templates with the method parameters.
*/
public static <I> I generate(final Class<I> klazz,
final ExecutorService es,
final MailMerge mm,
final String senderEmail,
final Map<String, String> constantVars,
final Map<String, String> templates) {
final MailerProxy m = new MailerProxy(klazz, es, mm, senderEmail, constantVars, templates);
return (I) Proxy.newProxyInstance(klazz.getClassLoader(), new Class[] { klazz }, m);
}
}