Project: * Clean-up (Eclipse cruft, unused files, etc...) * Git-specific changes * Maven POMs clean-up and changes for the build system * Version set to 1.0.0-0 in the development branches * Maven plug-ins updated to latest versions * Very partial dev. documentation added

This commit is contained in:
Emmanuel BENOîT 2011-12-09 08:07:33 +01:00
parent c74e30d5ba
commit 0665a760de
1439 changed files with 1020 additions and 1649 deletions
legacyworlds-server-beans-mailer

View file

@ -0,0 +1,30 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>legacyworlds-server-beans</artifactId>
<groupId>com.deepclone.lw</groupId>
<version>1.0.0</version>
<relativePath>../legacyworlds-server-beans/pom.xml</relativePath>
</parent>
<artifactId>legacyworlds-server-beans-mailer</artifactId>
<name>Legacy Worlds - Server - Components - Mailer</name>
<version>${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build}</version>
<description>This package contains the mailer component, which uses LW's i18n system and Spring's mail sending interfaces.
It is capable of sending mails synchronously or asynchronously.</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<type>jar</type>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,170 @@
package com.deepclone.lw.beans.mailer;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.deepclone.lw.interfaces.mailer.AlreadySentException;
import com.deepclone.lw.interfaces.mailer.MailData;
import com.deepclone.lw.interfaces.mailer.MissingDataException;
import com.deepclone.lw.interfaces.mailer.NotSentException;
/**
* The mail data implementation consists in a single-use message sender that includes templating
* capabilities.
*
* @author tseeker
*/
class MailDataImpl
implements MailData
{
/* Regular expressions used to identify template variables */
private static final String camelCase = "[a-z]+([A-Z][a-z]*)*";
private static final String fieldName = camelCase + "(\\." + camelCase + ")*";
private static final String field = "\\$\\{(" + fieldName + ")\\}";
private static final Pattern fPattern = Pattern.compile( field );
/** Queue-handling task */
private final MailQueueHandler mailQueueTask;
/** Address of the message's recipient */
private final String mailTo;
/** Template string of the message */
private final String template;
/** Field values to use when replacing */
private Map< String , Object > fields = new HashMap< String , Object >( );
/** Flag indicating whether the message has already been sent */
private Boolean sent = false;
/**
* Initialise the instance's data then extract variable names from the template string.f
*
* @param task
* the queue-handling task
* @param target
* the address of the message's recipient
* @param template
* the message's template
*/
MailDataImpl( MailQueueHandler task , String target , String template )
{
this.mailQueueTask = task;
this.mailTo = target;
this.template = template;
this.parseTemplate( );
}
/**
* Extracts variable names from the template.
*/
private void parseTemplate( )
{
Matcher m = fPattern.matcher( this.template );
while ( m.find( ) ) {
this.fields.put( m.group( 1 ) , null );
}
}
/**
* Makes sure a message is never sent twice.
*
* @throws AlreadySentException
* if the message had already been sent.
*/
private void checkSent( )
throws AlreadySentException
{
synchronized ( this.sent ) {
if ( this.sent ) {
throw new AlreadySentException( );
}
this.sent = true;
}
}
/**
* Prepares the message's data.
*
* This method will check that the message has not been sent, then start replacing variables by
* their assigned values in the template. It will return the fully-substituted message text.
*
* @return the message's text after variable substitution
* @throws AlreadySentException
* if the message has already been sent
* @throws MissingDataException
* if one of the variables has no value
*/
private String prepareMail( )
throws AlreadySentException , MissingDataException
{
this.checkSent( );
String message = this.template;
for ( String fName : this.fields.keySet( ) ) {
Object value = this.fields.get( fName );
if ( value == null ) {
synchronized ( this.sent ) {
this.sent = false;
}
throw new MissingDataException( fName );
}
String regexp = "\\$\\{";
regexp += fName.replace( (CharSequence) "." , (CharSequence) "\\." );
regexp += "\\}";
Pattern p = Pattern.compile( regexp );
message = p.matcher( message ).replaceAll( value.toString( ).replace( "$" , "\\$" ) );
}
return message;
}
/* Documented in MailData interface */
@Override
public void queue( )
throws AlreadySentException , MissingDataException
{
this.mailQueueTask.queueMessage( this.mailTo , this.prepareMail( ) );
}
/* Documented in MailData interface */
@Override
public void sendNow( )
throws AlreadySentException , NotSentException , MissingDataException
{
this.mailQueueTask.sendMessage( this.mailTo , this.prepareMail( ) );
}
/* Documented in MailData interface */
@Override
public void setData( String identifier , Object value )
{
if ( !this.fields.containsKey( identifier ) ) {
throw new IllegalArgumentException( "unknown field '" + identifier + "'" );
}
if ( this.fields.get( identifier ) != null ) {
throw new IllegalArgumentException( "field '" + identifier + "' already set" );
}
if ( value == null ) {
throw new IllegalArgumentException( "field value may not be null" );
}
this.fields.put( identifier , value );
}
}

View file

@ -0,0 +1,52 @@
package com.deepclone.lw.beans.mailer;
import com.deepclone.lw.interfaces.mailer.NotSentException;
/**
* Mail queue handler interface
*
* This interface is normally implemented by the mail queue task. However, it is required in order
* to test the MailData implementation separately.
*
* @author tseeker
*/
interface MailQueueHandler
{
/**
* Stops the queue-handling task.
*
* This method inserts a terminator into the queue, then waits for the queue to be
* de-initialised (which indicates that the main loop ended).
*/
public void stop( );
/**
* Adds a message to the queue.
*
* @param mailTo
* the address of the email's recipient.
* @param message
* the contents of the email
*/
public void queueMessage( String mailTo , String message );
/**
* Sends a message.
*
* @param mailTo
* address of the email's recipient
* @param message
* contents of the message
* @throws NotSentException
* if the mail could not be sent for some reason
*/
public void sendMessage( String mailTo , String message )
throws NotSentException;
}

View file

@ -0,0 +1,13 @@
package com.deepclone.lw.beans.mailer;
/**
* This empty abstract class is used as the base class for both queued mails and termination
* signals.
*
* @author tseeker
*/
abstract class MailQueueItem
{
// EMPTY
}

View file

@ -0,0 +1,211 @@
package com.deepclone.lw.beans.mailer;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import com.deepclone.lw.cmd.admin.logs.LogLevel;
import com.deepclone.lw.interfaces.eventlog.SystemLogger;
import com.deepclone.lw.interfaces.mailer.NotSentException;
/**
* This class implements the queue-handling task of the mailer bean. This task is responsible for
* sending mail asynchronously, but it also provides a method allowing mails to be sent
* synchronously by {@link MailData} instances.
*
* @author tseeker
*/
class MailQueueTask
implements Runnable , MailQueueHandler
{
/** The bean's system logger */
private final SystemLogger logger;
/** The From: address */
private final String mailFrom;
/** The mail sender bean */
private final MailSender mailSender;
/** The mail queue */
private LinkedBlockingQueue< MailQueueItem > queue;
/** Copies the various data and references */
MailQueueTask( SystemLogger logger , String mailFrom , LinkedBlockingQueue< MailQueueItem > queue ,
MailSender mailSender )
{
this.logger = logger;
this.mailFrom = mailFrom;
this.queue = queue;
this.mailSender = mailSender;
}
/**
* Stops the queue-handling task.
*
* This method inserts a terminator into the queue, then waits for the queue to be
* de-initialised (which indicates that the main loop ended).
*/
@Override
public void stop( )
{
this.queue.add( new MailQueueTerminator( ) );
while ( this.queue != null ) {
Thread.yield( );
}
}
/**
* Adds a message to the queue.
*
* This method adds a new {@link QueuedMail} instance to the queue, initialising it using the
* specified parameters. Should the queueing fail for some reason, the error is logged.
*
* @param mailTo
* the address of the email's recipient.
* @param message
* the contents of the email
*/
@Override
public void queueMessage( String mailTo , String message )
{
try {
this.queue.put( new QueuedMail( mailTo , message ) );
this.logger.log( LogLevel.DEBUG , "queued mail to " + mailTo );
} catch ( InterruptedException e ) {
// This should never happen, as the queue is non-blocking on this end
this.logger.log( LogLevel.ERROR , "could not queue mail to " + mailTo , e );
}
this.logger.flush( );
}
/**
* Sends a message.
*
* This method splits the message into two parts (subject and body), then initialises the
* {@link SimpleMailMessage} instance corresponding to the mail, before finally trying to send
* it through the mail sender bean.
*
* @param mailTo
* address of the email's recipient
* @param message
* contents of the message
* @throws NotSentException
* if the mail could not be sent for some reason
*/
@Override
public void sendMessage( String mailTo , String message )
throws NotSentException
{
// Extract title and body from message
// FIXME: there are better ways to do this
String title = "";
int i = 0;
while ( i < message.length( ) && message.charAt( i ) != '\n' ) {
title += message.charAt( i );
i++;
}
while ( i < message.length( ) && message.charAt( i ) == '\n' ) {
i++;
}
String body = ( i == message.length( ) ) ? "" : message.substring( i );
// Write log if the title or the body are empty
if ( title.equals( "" ) ) {
this.logger.log( LogLevel.WARNING , "sending email with no title" );
}
if ( body.equals( "" ) ) {
this.logger.log( LogLevel.WARNING , "sending email with empty body" );
}
this.logger.flush( );
// Create actual mail data
SimpleMailMessage smm = new SimpleMailMessage( );
smm.setFrom( this.mailFrom );
smm.setTo( mailTo );
smm.setSubject( title );
smm.setText( body );
// Try to send it
synchronized ( this.mailSender ) {
try {
this.mailSender.send( smm );
} catch ( MailException e ) {
this.logger.log( LogLevel.INFO , "could not send mail to " + mailTo , e ).flush( );
throw new NotSentException( e );
}
}
this.logger.log( LogLevel.DEBUG , "sent mail to " + mailTo ).flush( );
}
/**
* Main queue-handling loop.
*
* This method contains the task's main loop. It constantly polls the queue for incoming
* information, sending queued email. All errors while sending messages are ignored. When a
* terminator is found on the queue, all remaining messages will be flushed and the main loop
* will exit.
*/
@Override
public void run( )
{
boolean keepRunning = true;
LinkedBlockingQueue< MailQueueItem > queue = this.queue;
while ( keepRunning ) {
MailQueueItem item;
// Wait for items to send
try {
item = queue.take( );
} catch ( InterruptedException e ) {
this.logger.log( LogLevel.WARNING , "mail queue consumer interrupted" ).flush( );
continue;
}
// Handle terminators
if ( item instanceof MailQueueTerminator ) {
keepRunning = false;
this.logger.log( LogLevel.INFO , "termination order received" ).flush( );
}
do {
// Send mail
if ( item instanceof QueuedMail ) {
QueuedMail mail = (QueuedMail) item;
try {
this.sendMessage( mail.mailTo , mail.message );
} catch ( NotSentException e ) {
// Unable to send; it's been logged, so we ignore it
}
}
// If we're terminating, flush the queue
if ( ! ( keepRunning || queue.isEmpty( ) ) ) {
boolean fail;
do {
try {
item = queue.take( );
fail = false;
} catch ( InterruptedException e ) {
fail = true;
}
} while ( fail );
}
} while ( !keepRunning && !queue.isEmpty( ) );
}
// Kill the queue
this.queue = null;
}
}

View file

@ -0,0 +1,14 @@
package com.deepclone.lw.beans.mailer;
/**
* This empty class is a mail queue item which indicates that the mail queue handling task should
* terminate.
*
* @author tseeker
*/
class MailQueueTerminator
extends MailQueueItem
{
// EMPTY
}

View file

@ -0,0 +1,140 @@
package com.deepclone.lw.beans.mailer;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailSender;
import com.deepclone.lw.cmd.admin.logs.LogLevel;
import com.deepclone.lw.interfaces.eventlog.*;
import com.deepclone.lw.interfaces.i18n.*;
import com.deepclone.lw.interfaces.mailer.*;
/**
* The mailer bean spawns a queue-handling task on initialisation. It is responsible for
* initialising mail data instances, giving them access to the queue-handling task. When the
* container destroys the bean, it inserts a terminator in the mail queue then waits for the task to
* end.
*
* @author tseeker
*/
public class MailerBean
implements Mailer , InitializingBean , DisposableBean
{
/** The default From: address for mails sent by the bean */
private final static String defaultSender = "webmaster@legacyworlds.com";
/** Translator bean */
private Translator translator;
/** Spring mail sender bean */
private MailSender mailSender;
/** System logger for the mailer bean */
private SystemLogger logger;
/** From: address to use when sending mails */
private String mailFrom = defaultSender;
/** Mail queue */
private LinkedBlockingQueue< MailQueueItem > queue;
/** Queue-handling task */
private MailQueueTask task;
/**
* Sets the translator bean (DI)
*
* @param translator
* the translator bean
*/
@Autowired( required = true )
public void setTranslator( Translator translator )
{
this.translator = translator;
}
/**
* Sets the mail sender bean (DI)
*
* @param mailSender
* the mail sender bean
*/
@Autowired( required = true )
public void setMailSender( MailSender mailSender )
{
this.mailSender = mailSender;
}
/**
* Initialises the system logger (DI)
*
* @param logger
* the logger bean
*/
@Autowired( required = true )
public void setLogger( Logger logger )
{
this.logger = logger.getSystemLogger( "Mailer" );
}
/**
* Changes the From: address.
*
* @param mailFrom
* the new From: address
*/
public void setMailFrom( String mailFrom )
{
this.mailFrom = mailFrom;
}
/**
* Creates the mail queue and spawns the queue-handling task when the bean is initialised.
*/
@Override
public void afterPropertiesSet( )
{
this.logger.log( LogLevel.INFO , "mailer task initialising" ).flush( );
this.queue = new LinkedBlockingQueue< MailQueueItem >( );
this.task = new MailQueueTask( this.logger , this.mailFrom , this.queue , this.mailSender );
Thread t = new Thread( this.task );
t.start( );
}
/**
* Terminates and waits for the queue-handling task when the bean is destroyed.
*/
@Override
public void destroy( )
{
this.task.stop( );
this.task = null;
this.queue = null;
}
/* Documentation in Mailer interface */
@Override
public MailData createMail( String language , String contentsDef , String target )
throws TranslationException
{
String template = this.translator.translate( language , contentsDef );
return new MailDataImpl( this.task , target , template );
}
}

View file

@ -0,0 +1,26 @@
package com.deepclone.lw.beans.mailer;
/**
* This class represents a queued mail on the mail task's queue. It carries both the target address
* and the message's contents.
*
* @author tseeker
*/
class QueuedMail
extends MailQueueItem
{
/** The target mail address */
final String mailTo;
/** The contents of the email */
final String message;
/** Initialises the queued mail */
QueuedMail( String mailTo , String message )
{
this.mailTo = mailTo;
this.message = message;
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<import resource="mailer/mailer-bean.xml" />
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="127.0.0.1" />
</bean>
</beans>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- Mailer bean - all properties autowired or defaulted -->
<bean id="mailer" class="com.deepclone.lw.beans.mailer.MailerBean" />
</beans>