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