4/04/2012

filelistener [Part 2]

Looking at the last part of the filelistener project, the next thing for us to do is:
"Write a file listener that iterates through an input directory in a given interval and triggers a purely virtual function for someone else to override"
Here is the approach to implement these features, which I recommend saving as include/filelistener.hpp:

#ifndef FILELISTENER_HPP
#define FILELISTENER_HPP

/*
    Link with the following libraries:
    -lboost_filesystem
    -lpthread
*/

#include "datastructures.hpp"
#include <boost/asio.hpp>
#include <boost/optional.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

/**
 * Listens on a directory and triggers on parsed files.
 *
 * @author nijansen
 *
 * The listener will monitor the input directory in a given interval. All files within
 * the input directory will be read; if they can be parsed, the listener will call a
 * purely virtual trigger function and pass the parsed information.
 *
 * The listener is not recursive: Directories within the input directory will be
 * ignored. If files cannot be parsed, the listener will move them into an "ignored"
 * directory within the input directory.
 *
 * The listener will not throw exceptions on parsing failures. It will throw if it
 * lacks the necessary rights to read/write within the input directory. Exceptions
 * within the purely virtual trigger function must be caught by the derived class; the
 * listener will NOT catch those. This is intended to prevent unexpected behavior.
 *
 * The listener itself is thread safe. It is strongly recommended to use one io
 * service for each thread. Several listener objects may be registered to one io
 * service if they are in the same thread.
 *
 * @warning Do not set up multiple listener objects on the same input directory!
 */
class FileListener {
public:
    /**
     * Constructor that registers to an Boost Asio io service.
     *
     * The derived class must overload this constructor and call the parent constructor
     * with a valid io service.
     *
     * @param[in] io_service Asio io service the listener will register to.
     */
    FileListener(boost::asio::io_service &io_service);

    /**
     * Sets the input directory.
     *
     * @param[in] input Must be a valid (existing) directory.
     * @return true if successful, false if invalid.
     */
    bool set_input_directory(const boost::filesystem::path &input);

    /**
     * Sets the listening interval.
     *
     * @param[in] interval_ms Listening interval in milliseconds.
     */
    void set_listening_interval(long interval_ms);

    /**
     * Tries to start the listener service.
     *
     * @return true if successful.
     *
     * @warning The input directory and listening interval must be set!
     * @warning You must run the io service after this call!
     */
    bool start_service();

    /**
     * Purely virtual trigger function for derived classes.
     *
     * Derived classes must implement this function. It will be called by the
     * listener when a file was parsed successfully. The return value determines
     * whether the file will be deleted.
     *
     * @param[in] tournament Parsed data from the file.
     * @return 0 if successful (file is deleted!), any other value if failure.
     *
     * @warning On success return value, the file will be deleted permanently!
     */
    virtual int trigger(const datastructures::Tournament &tournament) = 0;
private:
    /**
     * Iterates through the input directory, and triggers on parsed files.
     */
    void listen();

    /** Input directory; default: unset. **/
    boost::optional<boost::filesystem::path> input_;
    /** Listening interval; default: unset. **/
    boost::optional<boost::posix_time::time_duration> interval_;
    /** Timer to listen in intervals. **/
    boost::asio::deadline_timer timer_;
};

#endif // FILELISTENER_HPP


And here is the respective implementation source/filelistener.cpp:

#include "filelistener.hpp"
#include <boost/bind.hpp>

FileListener::FileListener(boost::asio::io_service &io_service)
    : timer_(io_service)
{}

bool FileListener::set_input_directory(const boost::filesystem::path &input) {
    // Only directories are valid input paths
    if(boost::filesystem::is_directory(input)) {
        input_ = input;
        return true;
    }
    return false;
}

void FileListener::set_listening_interval(long interval_ms) {
    interval_ = boost::posix_time::milliseconds(interval_ms);
}

bool FileListener::start_service() {
    // Only start the server if the input path and listening interval have been set
    if(input_ && interval_) {
        // Set the timer
        timer_.expires_from_now(*interval_);
        timer_.async_wait(boost::bind(&FileListener::listen, this));
        return true;
    }
    return false;
}

void FileListener::listen() {
    // Iterate through the input directory
    for(boost::filesystem::directory_iterator file(*input_);
        file != boost::filesystem::directory_iterator(); file++)
    {
        boost::filesystem::path current_file = file->path();
        // Ignore directories
        if(boost::filesystem::is_directory(current_file)) {
            continue;
        }
        // Try to parse the file
        datastructures::Tournament tournament;
        try {
            tournament.load(current_file.native());
        }
        catch(...) {
            // For performance reasons, move files that failed parsing into input/ignored,
            // so we will not try to parse them over and over again
            boost::filesystem::path ignored = *input_/"ignored";
            if(!boost::filesystem::exists(ignored)) {
                boost::filesystem::create_directory(ignored);
            }
            boost::filesystem::copy_file(current_file, ignored/current_file.filename());
            boost::filesystem::remove(current_file);
            continue;
        }
        // Call the trigger function of the derived class
        if(trigger(tournament) == 0) {
            // Only delete the file if the trigger function submitted the success value;
            // the file will trigger again later otherwise
            boost::filesystem::remove(current_file);
        }
    }
    // Reset the timer
    timer_.expires_from_now(*interval_);
    timer_.async_wait(boost::bind(&FileListener::listen, this));
}


This leaves us with only one more step for this project: an example implementation. Coming up in a future post, of course.

No comments:

Post a Comment