Package mireka.pop.store

Source Code of mireka.pop.store.Maildrop$Appender

package mireka.pop.store;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.annotation.concurrent.GuardedBy;

import mireka.smtp.EnhancedStatus;
import mireka.transmission.LocalMailSystemException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Maildrop provides the physical storage for a specific maildrop (assigned to a
* user) using the file system. This implementation uses a single directory,
* where it stores each mail in separate files in addition to some control
* files. All operations are as atomic as possible, preventing leaving the
* maildrop in an inconsistent state.
*/
public class Maildrop {
    private final Logger logger = LoggerFactory.getLogger(Maildrop.class);
    private final String name;
    private final File dir;
    @GuardedBy("this")
    private boolean isTransactionRunning;
    @GuardedBy("this")
    private int appenderCount;
    @GuardedBy("this")
    /**
     * true if data from disk is already loaded
     */
    private boolean isInitialized;
    /**
     * Sorted list of messages ordered by the numeric value of the file name.
     * Items must not be appended if a transaction is running.
     */
    @GuardedBy("this")
    private List<Message> messages;
    @GuardedBy("this")
    private final List<Message> pendingMessages =
            new ArrayList<Maildrop.Message>();
    @GuardedBy("this")
    private final UidManager uidManager;

    public Maildrop(String name, File maildropDir) {
        this.name = name;
        this.dir = maildropDir;
        this.uidManager = new UidManager(maildropDir);
    }

    public synchronized void beginTransaction() throws MaildropLockedException,
            MaildropPopException {
        if (isTransactionRunning)
            throw new MaildropLockedException();
        try {
            initialize();
        } catch (MaildropException e) {
            logger.error("Cannot read maildrop content from disk", e);
            throw new MaildropPopException("SYS/PERM", "Corrupted mailbox");
        }
        isTransactionRunning = true;
    }

    /**
     * Reads persistent maildrop data from the disk if it is not already read
     *
     * @throws MaildropException
     */
    private void initialize() throws MaildropException {
        if (isInitialized)
            return;

        if (!isFullyConstructed())
            constructNewMaildropDir();

        uidManager.init();
        readDirectory();
        isInitialized = true;
    }

    private boolean isFullyConstructed() {
        return new File(dir, "constructed").exists();
    }

    private void constructNewMaildropDir() throws MaildropException {
        if (!dir.isDirectory()) {
            boolean success = dir.mkdir();
            if (!success)
                throw new MaildropException("Cannot create maildrop directory "
                        + dir);
        }
        uidManager.createInitialUidFile();
        File constructedFile = new File(dir, "constructed");
        try {
            boolean created = constructedFile.createNewFile();
            if (!created)
                throw new RuntimeException("Assertion failed");
        } catch (IOException e) {
            throw new MaildropException(
                    "The 'successful' marker file cannot be created, "
                            + "maildrop is invalid " + constructedFile);
        }
    }

    private void readDirectory() throws MaildropException {
        int countOfDeletedTemporaryFiles = 0;
        messages = new ArrayList<Message>();
        File[] files = dir.listFiles();
        if (files == null)
            throw new MaildropException(
                    "Cannot list mail files in maildrop at " + dir);
        for (File file : files) {
            String name = file.getName();
            if (name.startsWith("temp.mail.")) {
                boolean success = file.delete();
                if (success) {
                    countOfDeletedTemporaryFiles++;
                } else {
                    throw new MaildropException(
                            "Temporary mail file cannot be "
                                    + "deleted, maildrop is invalid " + file);
                }
            }
            if (!Character.isDigit(name.charAt(0)))
                continue;
            if (!name.endsWith(".eml"))
                throw new MaildropException("Invalid mail file name: " + file);
            String baseName = name.substring(0, name.length() - 4);
            Message message = new Message();
            try {
                message.id = Long.valueOf(baseName);
            } catch (NumberFormatException e) {
                throw new MaildropException("Invalid mail file name: " + file);
            }
            message.length = file.length();
            if (message.length == 0)
                throw new MaildropException("Cannot determine length of file "
                        + file);
            messages.add(message);
        }
        Collections.sort(messages, new MessageIdComparator());
        if (countOfDeletedTemporaryFiles > 0)
            logger.warn(countOfDeletedTemporaryFiles
                    + " temporary mail files were deleted in " + dir);
    }

    public synchronized long getCountOfMessages() {
        if (!isTransactionRunning)
            throw new IllegalStateException("Assertion failed");

        long result = 0;
        for (Message message : messages) {
            if (!message.deleted)
                result++;
        }
        return result;
    }

    public synchronized long getTotalOctets() {
        if (!isTransactionRunning)
            throw new IllegalStateException("Assertion failed");

        long result = 0;
        for (Message message : messages) {
            if (!message.deleted)
                result += message.length;
        }
        return result;
    }

    public synchronized List<ScanListing> getScanListings() {
        if (!isTransactionRunning)
            throw new IllegalStateException("Assertion failed");

        List<ScanListing> result = new ArrayList<ScanListing>();
        for (int i = 0; i < messages.size(); i++) {
            Message message = messages.get(i);
            if (!message.deleted)
                result.add(new ScanListing(i + 1, message.length));
        }
        return result;
    }

    public synchronized ScanListing getScanListing(int messageNumber)
            throws MaildropPopException, IllegalStateException {
        Message message = getMessageByMessageNumber(messageNumber);
        return new ScanListing(messageNumber, message.length);
    }

    public synchronized List<UidListing> getUidListings() {
        if (!isTransactionRunning)
            throw new IllegalStateException("Assertion failed");

        List<UidListing> result = new ArrayList<UidListing>();
        for (int i = 0; i < messages.size(); i++) {
            Message message = messages.get(i);
            if (!message.deleted)
                result.add(new UidListing(i + 1, message.id));
        }
        return result;
    }

    public synchronized UidListing getUidListing(int messageNumber)
            throws MaildropPopException, IllegalStateException {
        Message message = getMessageByMessageNumber(messageNumber);
        return new UidListing(messageNumber, message.id);
    }

    public synchronized InputStream getMailAsStream(int messageNumber)
            throws MaildropPopException, IllegalStateException {
        Message message = getMessageByMessageNumber(messageNumber);
        try {
            return new FileInputStream(message.getFile());
        } catch (FileNotFoundException e) {
            logger.error("Cannot open mail file " + message.getFile(), e);
            throw new MaildropPopException("SYS/PERM", "Corrupted mailbox");
        }
    }

    /**
     * Throws an exception if the maildrop is not in transaction mode, the
     * message number is invalid, or refers to a deleted mail.
     */
    private Message getMessageByMessageNumber(int messageNumber)
            throws MaildropPopException, IllegalStateException {
        if (!isTransactionRunning)
            throw new IllegalStateException();
        if (messageNumber < 1 || messageNumber > messages.size())
            throw new MaildropPopException(null, "No such message");
        Message message = messages.get(messageNumber - 1);
        if (message.deleted)
            throw new MaildropPopException(null, "Deleted message");
        return message;
    }

    public synchronized void delete(int messageNumber)
            throws MaildropPopException, IllegalStateException {
        Message message = getMessageByMessageNumber(messageNumber);
        message.deleted = true;
    }

    public synchronized void resetDeletions() {
        resetDeletedFlags();
    }

    private void resetDeletedFlags() {
        for (Message message : messages) {
            message.deleted = false;
        }
    }

    public synchronized void commitTransaction() throws MaildropPopException,
            IllegalStateException {
        if (!isTransactionRunning)
            throw new IllegalStateException();

        try {
            removeDeletedMessages();
        } finally {
            appendPendingMessages();
            isTransactionRunning = false;
        }
    }

    private void removeDeletedMessages() throws MaildropPopException {
        List<Message> keptMessages = new ArrayList<Maildrop.Message>();
        int cFailedDeletions = 0;
        for (Message message : messages) {
            if (message.deleted) {
                boolean success = message.getFile().delete();
                if (!success) {
                    message.deleted = false;
                    keptMessages.add(message);
                    cFailedDeletions++;
                    if (cFailedDeletions <= 3)
                        logger.error("Mail cannot be deleted: "
                                + message.getFile());
                }
            } else {
                keptMessages.add(message);
            }
        }
        messages = keptMessages;

        if (cFailedDeletions > 3)
            logger.error((cFailedDeletions - 3)
                    + " additional mails cannot be deleted");
        if (cFailedDeletions != 0)
            throw new MaildropPopException("SYS/PERM", "Cannot remove "
                    + cFailedDeletions + " deleted mails");
    }

    private void appendPendingMessages() {
        messages.addAll(pendingMessages);
        pendingMessages.clear();
    }

    public synchronized void rollbackTransaction() throws IllegalStateException {
        if (!isTransactionRunning)
            throw new IllegalStateException();

        resetDeletedFlags();
        appendPendingMessages();
        isTransactionRunning = false;
    }

    public synchronized MaildropAppender allocateAppender()
            throws LocalMailSystemException {
        try {
            initialize();
        } catch (MaildropException e) {
            throw new LocalMailSystemException(e,
                    EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
        }
        appenderCount++;
        return new Appender();
    }

    public synchronized void checkReleasedState() {
        if (isTransactionRunning)
            throw new IllegalStateException("Properly released maildrop was "
                    + "expected, but a transaction is still running: " + name
                    + ", " + dir);
        if (appenderCount >= 1)
            throw new IllegalStateException("Properly released maildrop was "
                    + "expected, but an appender is still not released: "
                    + name + ", " + dir);
    }

    /**
     * @category GETSET
     */
    public String getName() {
        return name;
    }

    private class Message {
        /**
         * UID
         */
        long id;
        /**
         * Mail length in bytes.
         */
        long length;
        /**
         * True if the mail is marked for deletion in the currently running
         * transaction.
         */
        boolean deleted;

        File getFile() {
            return new File(dir, id + ".eml");
        }
    }

    /**
     * This class compares {@link Message} instances based on there UID.
     */
    private static class MessageIdComparator implements Comparator<Message> {

        @Override
        public int compare(Message o1, Message o2) {
            return Long.signum(o1.id - o2.id);
        }
    }

    private class Appender implements MaildropAppender {
        private AppenderStatus status = AppenderStatus.NEW;
        private long uid;
        private File tempFile;
        private File finalFile;
        private FileOutputStream outputStream;

        @Override
        public OutputStream getOutputStream() throws LocalMailSystemException {
            if (status != AppenderStatus.NEW)
                throw new IllegalStateException();
            synchronized (Maildrop.this) {
                try {
                    allocateFileNames();
                } catch (InvalidUidFileException e) {
                    throw new LocalMailSystemException(e,
                            EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
                }
            }
            try {
                outputStream = new FileOutputStream(tempFile);
            } catch (FileNotFoundException e) {
                throw new LocalMailSystemException(e,
                        EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
            }
            status = AppenderStatus.OPEN;
            return outputStream;
        }

        private void allocateFileNames() throws InvalidUidFileException {
            uid = uidManager.allocateUid();
            tempFile = new File(dir, "temp.mail." + uid + ".eml");
            finalFile = new File(dir, uid + ".eml");
        }

        @Override
        public void commit() throws LocalMailSystemException {
            if (status == AppenderStatus.CLOSED)
                return;
            try {
                if (status == AppenderStatus.NEW)
                    return;

                // status is OPEN
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new LocalMailSystemException(e,
                            EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
                }
                Message message = new Message();
                message.id = uid;
                message.length = tempFile.length();
                if (message.length == 0)
                    throw new LocalMailSystemException(
                            "Cannot retrieve file length " + tempFile,
                            EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
                boolean success = tempFile.renameTo(finalFile);
                if (!success) {
                    if (tempFile.delete()) {
                        throw new LocalMailSystemException(
                                "Cannot move temporary file to final "
                                        + "destination, but at least it could be deleted "
                                        + tempFile,
                                EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
                    } else {
                        throw new LocalMailSystemException(
                                "Cannot move temporary file to final destination, nor it can be deleted "
                                        + tempFile,
                                EnhancedStatus.TRANSIENT_LOCAL_ERROR_IN_PROCESSING);
                    }
                }
                synchronized (Maildrop.this) {
                    pendingMessages.add(message);
                    if (!isTransactionRunning) {
                        appendPendingMessages();
                    }
                }
                logger.debug("Message " + message.id + " is added to maildrop "
                        + name);
            } finally {
                synchronized (Maildrop.this) {
                    if (appenderCount < 1)
                        throw new RuntimeException("Assertion failed");
                    appenderCount--;
                }
                status = AppenderStatus.CLOSED;
            }
        }

        public void rollback() {
            if (status == AppenderStatus.CLOSED)
                return;
            try {
                if (status == AppenderStatus.NEW)
                    return;

                // status is OPEN
                try {
                    outputStream.close();
                } catch (IOException e) {
                    logger.error("Cannot close temporary file " + tempFile, e);
                    return;
                }
                boolean success = tempFile.delete();
                if (!success) {
                    logger.error("Cannot delete temporary file " + tempFile);
                    return;
                }
                logger.debug("Maildrop '" + name
                        + "' appender transaction for UID " + uid
                        + " is rolled back, temporary file is deleted.");
                return;
            } finally {
                synchronized (Maildrop.this) {
                    if (appenderCount < 1)
                        throw new RuntimeException("Assertion failed");
                    appenderCount--;
                }
                status = AppenderStatus.CLOSED;
            }

        }
    }

    private enum AppenderStatus {
        NEW, OPEN, CLOSED
    };
}
TOP

Related Classes of mireka.pop.store.Maildrop$Appender

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.