/** * (c) Copyright 2007-2010 by emarsys eMarketing Systems AG * * This file is part of dyson. * * dyson is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * dyson is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.emarsys.dyson.storage; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emarsys.dyson.Dyson; import com.emarsys.dyson.DysonConfig; import com.emarsys.dyson.DysonException; import com.emarsys.dyson.DysonStorage; import com.emarsys.dyson.MailStorageFileNamingScheme; import com.emarsys.dyson.DysonStatistics.MailEvent; import com.emarsys.ecommon.concurrent.Threads; import com.emarsys.ecommon.prefs.config.Configuration; import com.emarsys.ecommon.time.Dates; import com.emarsys.ecommon.util.Assertions; import com.emarsys.ecommon.util.StopableRunnable; /** * The default implementation of {@link DysonStorage}. * * TODO documentation * * @author <a href="mailto:kulovits@emarsys.com">Michael "kULO" Kulovits</a> */ public class DefaultDysonStorage extends DysonStorage { private static Logger log = LoggerFactory.getLogger( DefaultDysonStorage.class ); public static final String LOCK_FILE_NAME = ".lock"; /** * Processor for the delivered mails in the * {@link DysonStorage#incomingDirName incoming folder}. * * @author <a href="mailto:kulovits@emarsys.com">Michael "kULO" Kulovits</a> */ protected class DeliveredMailProcessor implements StopableRunnable { private volatile boolean shouldStop = false; private volatile boolean isRunning = false; /** * * @see com.emarsys.ecommon.util.StopableRunnable#stop() */ public synchronized void stop() { this.shouldStop = true; log.info( "stopping delivered mail processor..." ); } /** * * @see java.lang.Runnable#run() */ public void run() { try { log.info( "started delivered mail processor" ); this.isRunning = true; while( !this.shouldStop ) { moveDeliveredMailsIntoProcessedDir(); //TODO use blocking (e.g.: wait and notify) instead of sleeping Threads.sleepSilently( Dates.SECOND_IN_MILLIS ); } } finally { this.isRunning = false; fireDeliveredMailProcessorStopped(); log.info( "stopped delivered mail processor!" ); } } /** * @return the isRunning */ public boolean isRunning() { return isRunning; } }//class DeliveredMailProcessor //monitors for synchronization protected final Object lifecycleMonitor = new Object(); protected final Object fileCopyMonitor = new Object(); //worker thread(service)s protected ExecutorService storageService; protected DeliveredMailProcessor deliveredMailProcessor; /** * * @param dyson */ public DefaultDysonStorage( Dyson dyson ) { super( dyson ); } /** * @see DysonStorage#init() */ @Override protected void init() throws IllegalStateException { this.setupFileStorage(); } /** * <p> * Starts the {@link DysonStorage}. * </p><p> * This will {@link #startStorageServices() start} the * storage services ({@link #storageService}, * {@link #deliveredMailProcessor}), too. * </p> */ public void start() { synchronized( this.lifecycleMonitor ) { if( this.isRunning() ) { throw new IllegalStateException( "Cannot (re)start dyson storage which is " + "already/still running!"); } log.info( "starting dyson storage component..."); this.lockStorageDirs(); this.startStorageServices(); } } /** * Checks whether this dyson storage is (still) running. * * * @return if both the {@link #deliveredMailProcessor} thread * as well as the {@link #storageService} is running * (.i.e. {@link ExecutorService#isTerminated() not * (yet)terminated}) */ public boolean isRunning() { synchronized( this.lifecycleMonitor ) { boolean isServiceRunning = false; boolean isMailProcessorRunning = false; if( this.storageService != null && !this.storageService.isTerminated() ) { isServiceRunning = true; } if( this.deliveredMailProcessor != null && this.deliveredMailProcessor.isRunning() ) { isMailProcessorRunning = true; } return isServiceRunning && isMailProcessorRunning; } } /** * Stops the dyson storage. * * Sends and asynchronous shutdown request to the * {@link #storageService} as well as to the * {@link #deliveredMailProcessor}. * */ public void stop() { synchronized( this.lifecycleMonitor ) { if( isRunning() ) { log.info("stopping dyson storage component..."); if( this.storageService != null ) { this.storageService.shutdown(); } if( this.deliveredMailProcessor != null ) { this.deliveredMailProcessor.stop(); } this.unlockStorageDirs(); } else { log.info( "dyson storage was not running - " + "nothing to shutdown!"); } } } /** * */ protected void setupFileStorage() { synchronized( this.lifecycleMonitor ) { Configuration config = this.getDyson().getConfiguration(); //get directory name for incoming mail if( this.incomingDirName == null ) { this.incomingDirName = config.get( DysonConfig.STORAGE_DIR_INCOMING ).getValue(); this.createDirsIfNotPresent( this.incomingDirName ); } //get directory name for already processed mail if( this.processedDirName == null ) { this.processedDirName = config.get( DysonConfig.STORAGE_DIR_PROCESSED ).getValue(); this.createDirsIfNotPresent( this.processedDirName ); } //get the suffix for mail files if( this.mailFileSuffix == null ) { this.mailFileSuffix = config.get( DysonConfig.STORAGE_MAIL_FILE_SUFFIX ).getValue(); } //get the suffix for patial mail files if( this.mailPartialFileSuffix == null ) { this.mailPartialFileSuffix = config.get( DysonConfig.STORAGE_MAIL_PARTIAL_FILE_SUFFIX ).getValue(); } //setup the naming scheme implementation if( this.namingScheme == null ) { this.namingScheme = dyson.newDysonPart( DysonConfig.STORAGE_PROCESSED_MAIL_NAMING_SCHEME_CLASS, MailStorageFileNamingScheme.class ); } } } /** * Creates the directory with the passed filename if it's not * already present. * * @param pathToDir - the path to the directory * @throws DysonException - if it was not possible to create a * writable directory with the passed path. */ protected void createDirsIfNotPresent( String pathToDir ) throws DysonException { File dir = new File( pathToDir ); if( !dir.exists() ) { log.debug( "creating dir(s) \'{}\'", pathToDir ); dir.mkdirs(); dir.setReadable( true ); dir.setWritable( true ); } boolean isWritableDirectoryPresent = dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite(); if( !isWritableDirectoryPresent ) { throw new DysonException( "Was not able to create directory \'" + pathToDir + "\'" ); } } protected void lockStorageDirs() throws DysonException { this.lock( this.getIncomingDirName() ); this.lock( this.getProcessedDirName() ); } protected void unlockStorageDirs() { this.unlock( this.getIncomingDirName() ); this.unlock( this.getProcessedDirName() ); } protected File getLockFileForStorageDir( String storageDirName ) { Assertions.assertNotNull( storageDirName ); return new File( storageDirName + File.separator + LOCK_FILE_NAME ); } protected void lock( String storageDirName ) { try { log.debug( "locking storage directory " + storageDirName ); File lockFile = this.getLockFileForStorageDir( storageDirName ); if( lockFile.exists() ) { throw new DysonException( storageDirName + " is already locked by process " + this.getLockingProcess( lockFile ) ); } this.writeLockFile( lockFile ); } catch( IOException ioe ) { final String msg = "unable to lock " + storageDirName + ": " + ioe; log.error( msg, ioe ); throw new DysonException( msg, ioe ); } } protected void unlock( String storagDirName ) { File lockfile = this.getLockFileForStorageDir( storagDirName ); if( lockfile.exists() ) { boolean deleted = lockfile.delete(); log.debug( "{} lock file {}", deleted ? "successfully deleted" : "could not delete", lockfile.getAbsolutePath() ); } else { log.warn( "cannot remove lock file {} which does not exist!", lockfile.getAbsolutePath() ); } } protected String getLockingProcess( File lockFile ) throws IOException { if( !lockFile.exists() ) { throw new IllegalStateException( "lockfile " + lockFile.getAbsolutePath() + " does not" + "(no longer?) exist!" ); } List<?> lines = FileUtils.readLines( lockFile ); return lines.isEmpty() ? "unknown" : lines.get( 0 ).toString(); } protected void writeLockFile( File lockFile ) throws IOException { FileUtils.writeStringToFile( lockFile, this.getPID() ); } /** * ugly hack to get the PID, only works in SUN VMs */ protected String getPID() { String pid = ManagementFactory.getRuntimeMXBean().getName(); return pid.substring( 0, pid.indexOf( '@' ) ); } /** * Creates and starts the {@link #storageService * storage's executor service} as well as the * {@link #deliveredMailProcessor} in its own {@link Thread}. * */ protected void startStorageServices() { if( this.storageService == null ) { this.storageService = new ThreadPoolExecutor( 0, Integer.MAX_VALUE, 5L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } if( this.deliveredMailProcessor == null ) { this.deliveredMailProcessor = new DeliveredMailProcessor(); } if( !this.deliveredMailProcessor.isRunning() ) { Thread th = new Thread( this.deliveredMailProcessor, "DeliveredMailProcessor" ); th.start(); } } /** * */ protected void fireDeliveredMailProcessorStopped() { synchronized( this.lifecycleMonitor ) { this.deliveredMailProcessor = null; } } /** * <p> * {@link #move(File, File) Moves} all already delivered mails * from the incoming folder to its final storage location in a * subfolder of the processed directory. * </p><p> * * </p> * * @see #move(File, File) */ protected void moveDeliveredMailsIntoProcessedDir() { synchronized( this.fileCopyMonitor ) { File processedDir = new File( this.processedDirName ); for( File mail : this.getIncomingMailFiles() ) { this.move( mail, processedDir ); } } } /** * * @param movee * @param toDir */ protected void move( final File movee, final File toDir ) { Runnable mover = new Runnable() { public void run() { boolean successful = false; Exception ex = null; File targetFile = null; try { log.debug( "moving {} to {}", movee.getAbsolutePath(), toDir.getAbsolutePath() ); targetFile = namingScheme.getMailFile( toDir, new FileInputStream( movee ) ); log.debug( "created storage file \'{}\' for \'{}\'", targetFile.getAbsolutePath(), movee.getAbsoluteFile() ); createDirsIfNotPresent( targetFile.getParent() ); successful = movee.renameTo( targetFile ); } catch( Exception ex2 ) { ex = ex2; successful = false; } final String from = movee.getAbsolutePath(); final String to = (targetFile == null ) ? "null" : targetFile.getAbsolutePath(); if( successful ) { log.debug( "successfully moved {} to {}", from, to ); getDyson().getStatistics().fire( MailEvent.MAIL_PROCESSED ); } else { log.error( "cannot move " + from + " to " + to + " (exception: " + ex + ")", ex ); } } }; this.storageService.submit( mover ); } /** * Removes all files from the incoming directory. * * TODO implement locking with the {@link IncomingStorageMessageListener}s * @throws IOException */ public void clearIncomingDir() throws IOException { log.info( "cleaning incoming dir \'{}\'", this.incomingDirName ); FileUtils.cleanDirectory( new File( this.incomingDirName) ); } /** * Removes all files from the processed directory. * @throws IOException */ public void clearProcessedDir() throws IOException { synchronized( this.fileCopyMonitor ) { log.info( "cleaning processed dir \'{}\'", this.processedDirName); FileUtils.cleanDirectory( new File( this.processedDirName ) ); } } /** * @see DysonStorage#awaitTermination(int, TimeUnit) */ @Override public void awaitTermination(int timeOut, TimeUnit unit) { Threads.awaitTerminationSilently( this.storageService, timeOut, unit ); } /** * @see DysonStorage#submitTask(Runnable) */ @Override public void submitTask( Runnable task ) throws IllegalStateException { if( !this.isRunning() ) { throw new IllegalStateException( "cannot submit task, storage is not running!" ); } this.storageService.submit( task ); } }//class DefaultDysonStorage