com.emarsys.dyson.storage.DefaultDysonStorage.java Source code

Java tutorial

Introduction

Here is the source code for com.emarsys.dyson.storage.DefaultDysonStorage.java

Source

/**
 *   (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