3/22/2012

filelistener [Part 1]

This time, as promised, we will look at a small project, rather than a snippet. Imagine the following context:

A customer is hosting a website organizing esport tournaments. He is providing us with xml files for each tournament, containing information about the tournament's name, its sponsors, the participating clans and their players, etc. Our job is to write a file listener, that will notice when a new xml file is saved in an input directory, try to parse this file, and sends triggers so someone else can implement something useful to do with the parsed information.

Our task will be split into these parts:
  1. Defining the datastructures used within the XML files and write a parser for these files
  2. 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
  3. Construct a very simple demonstration of how to implement the purely virtual function and embed it into a fully functional program
Lets dive right into datastructures: we can store the information within structs. For example:

struct Sponsor {
    std::string name;
    std::string description;
    std::string url_website;
    std::string url_image;
};

However, we notice by looking through the example XML files provided by our customer that most values are optional, and may not be referenced at all. We will need to deal with these optional values somehow, and the from reading my last post, you already know one possible approach: pointers. While using pointers for optional values is a thoughtful approach, my first post was dedicated to explaining why large pointer structures are inconvenient to work with. There must be a better approach for this. Of course, Boost does have a library for this problem: Boost Optional. It is ridiculously easy to apply:

struct Sponsor {
    std::string name;
    boost::optional<std::string> description;
    boost::optional<std::string> url_website;
    boost::optional<std::string> url_image;
};

Handling these values feels naturally, using the dereference operator makes them feel a lot like the pointers used by gSOAP, but we can, for example, assign values to them directy, do not have to worry about allocating any memory for them, etc. Now we already have the structure in which we can store our information. What is missing is the parsing. Parsing is a rather complicated field, but, of course, Boost has it done for us already with the Boost Property Tree. All we have to do is add a load (and for demonstration purposes also a save) function, adjust it to our structures and we are good to go. Here is an example interface; if you want to compile the project afterwards I recommend saving it to include/datastructures.hpp:

#ifndef DATASTRUCTURES_HPP
#define DATASTRUCTURES_HPP

#include <boost/property_tree/ptree.hpp>
#include <boost/optional.hpp>
#include <string>
#include <vector>

namespace datastructures {
    using boost::property_tree::ptree;

    struct BaseStructure {
        std::string name;
        boost::optional<std::string> description;
        boost::optional<std::string> url_website;
        boost::optional<std::string> url_image;

        virtual void load(const ptree &pt) = 0;
        virtual ptree save() const = 0;
    };

    struct Player : BaseStructure {
        boost::optional<std::string> first_name;
        boost::optional<std::string> last_name;
        boost::optional<int> age;

        void load(const ptree &pt);
        ptree save() const;
    };

    struct Clan : BaseStructure {
        std::vector<Player> players;

        void load(const ptree &pt);
        ptree save() const;
    };

    struct Sponsor : BaseStructure {
        void load(const ptree &pt);
        ptree save() const;
    };

    struct Tournament : BaseStructure {
        std::vector<Sponsor> sponsors;
        std::vector<Clan> clans;

        void load(const ptree &pt);
        void load(const std::string &filename);
        ptree save() const;
        void save(const std::string &filename) const;
    };
}

#endif // DATASTRUCTURES_HPP

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

#include "datastructures.hpp"
#include <boost/property_tree/xml_parser.hpp>
#include <boost/foreach.hpp>

using namespace datastructures;

void Player::load(const ptree &pt) {
    name = pt.get<std::string>("name");
    description = pt.get_optional<std::string>("description");
    url_website = pt.get_optional<std::string>("url_website");
    url_image = pt.get_optional<std::string>("url_image");

    first_name = pt.get_optional<std::string>("first_name");
    last_name = pt.get_optional<std::string>("last_name");
    age = pt.get_optional<int>("age");
}

ptree Player::save() const {
    ptree pt;

    pt.put("name", name);
    if(description) pt.put("description", *description);
    if(url_website) pt.put("url_website", *url_website);
    if(url_image) pt.put("url_image", *url_image);

    if(first_name) pt.put("first_name", *first_name);
    if(last_name) pt.put("last_name", *last_name);
    if(age) pt.put("age", *age);

    return pt;
}

void Clan::load(const ptree &pt) {
    name = pt.get<std::string>("name");
    description = pt.get_optional<std::string>("description");
    url_website = pt.get_optional<std::string>("url_website");
    url_image = pt.get_optional<std::string>("url_image");

    BOOST_FOREACH(const ptree::value_type &val, pt.get_child("players")) {
        Player player;
        player.load(val.second);
        players.push_back(player);
    }
}

ptree Clan::save() const {
    ptree pt;

    pt.put("name", name);
    if(description) pt.put("description", *description);
    if(url_website) pt.put("url_website", *url_website);
    if(url_image) pt.put("url_image", *url_image);

    for(std::vector<Player>::const_iterator player = players.begin();
        player != players.end(); player++)
    {
        pt.add_child("players.player", player->save());
    }

    return pt;
}

void Sponsor::load(const ptree &pt) {
    name = pt.get<std::string>("name");
    description = pt.get_optional<std::string>("description");
    url_website = pt.get_optional<std::string>("url_website");
    url_image = pt.get_optional<std::string>("url_image");
}

ptree Sponsor::save() const {
    ptree pt;

    pt.put("name", name);
    if(description) pt.put("description", *description);
    if(url_website) pt.put("url_website", *url_website);
    if(url_image) pt.put("url_image", *url_image);

    return pt;
}

void Tournament::load(const ptree &pt) {
    name = pt.get<std::string>("name");
    description = pt.get_optional<std::string>("description");
    url_website = pt.get_optional<std::string>("url_website");
    url_image = pt.get_optional<std::string>("url_image");

    BOOST_FOREACH(const ptree::value_type &val, pt.get_child("sponsors")) {
        Sponsor sponsor;
        sponsor.load(val.second);
        sponsors.push_back(sponsor);
    }

    BOOST_FOREACH(const ptree::value_type &val, pt.get_child("clans")) {
        Clan clan;
        clan.load(val.second);
        clans.push_back(clan);
    }
}

void Tournament::load(const std::string &filename) {
    ptree pt;
    read_xml(filename, pt);
    load(pt.get_child("tournament"));
}

ptree Tournament::save() const {
    ptree pt;

    pt.put("name", name);
    if(description) pt.put("description", *description);
    if(url_website) pt.put("url_website", *url_website);
    if(url_image) pt.put("url_image", *url_image);

    for(std::vector<Sponsor>::const_iterator sponsor = sponsors.begin();
        sponsor != sponsors.end(); sponsor++)
    {
        pt.add_child("sponsors.sponsor", sponsor->save());
    }

    for(std::vector<Clan>::const_iterator clan = clans.begin();
        clan != clans.end(); clan++)
    {
        pt.add_child("clans.clan", clan->save());
    }

    return pt;
}

void Tournament::save(const std::string &filename) const {
    ptree pt;
    pt.put_child("tournament", save());
    write_xml(filename, pt);
}

Isn't this incredibly easy? You could, by the way, blame me for breaking the DRY idiom, there is definitely space for improvement in that implementation, but it serves the little demonstration here. If you have not yet worked with property trees, I recommend doing a bit of playing around with these datastructures, and the XML files they produce before we continue to the next step, that will follow up with the next post.

No comments:

Post a Comment