mgmt: added subscription-based configuration file parser
Modules subscribe to configuration sections they are interested
in by name and provide a callback function. After parsing
the configuration file, ConfigFile will iterate through the
top level tree and invoke all subscribed callbacks for each
section it comes across.
ConfigFile has no concept of the specific tags or structure of the file
other than there being a single root tag (not a specific name) under which
all sections fall.
refs: #1120
Change-Id: Ic06fbdc85a9ac9740d46a6c2457ff5a9bd38e8a5
diff --git a/daemon/common.hpp b/daemon/common.hpp
index 3d1a53a..18a33c9 100644
--- a/daemon/common.hpp
+++ b/daemon/common.hpp
@@ -24,6 +24,9 @@
#include <vector>
#include <list>
#include <set>
+#include <sstream>
+#include <istream>
+#include <fstream>
#include "core/logger.hpp"
diff --git a/daemon/mgmt/config-file.cpp b/daemon/mgmt/config-file.cpp
new file mode 100644
index 0000000..a55650e
--- /dev/null
+++ b/daemon/mgmt/config-file.cpp
@@ -0,0 +1,116 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+
+#include "config-file.hpp"
+
+#include <boost/property_tree/info_parser.hpp>
+
+namespace nfd {
+
+NFD_LOG_INIT("ConfigFile");
+
+ConfigFile::ConfigFile()
+{
+
+}
+
+void
+ConfigFile::addSectionHandler(const std::string& sectionName,
+ OnConfig subscriber)
+{
+ m_subscriptions[sectionName] = subscriber;
+}
+
+void
+ConfigFile::parse(const char* filename, bool isDryRun)
+{
+ std::ifstream inputFile;
+ inputFile.open(filename);
+ if (!inputFile.is_open())
+ {
+ std::string msg = "Failed to read configuration file: ";
+ msg += filename;
+ throw Error(filename);
+ }
+ parse(inputFile, isDryRun, filename);
+ inputFile.close();
+}
+
+void
+ConfigFile::parse(const std::string& input, bool isDryRun, const char* filename)
+{
+ std::istringstream inputStream(input);
+ parse(inputStream, isDryRun, filename);
+}
+
+
+void
+ConfigFile::parse(std::istream& input, bool isDryRun, const char* filename)
+{
+ try
+ {
+ boost::property_tree::read_info(input, m_global);
+ }
+ catch (const boost::property_tree::info_parser_error& error)
+ {
+ std::stringstream msg;
+ msg << "Failed to parse configuration file";
+ if (filename != 0)
+ {
+ msg << " " << filename;
+ }
+ msg << " " << error.message() << " line " << error.line();
+ throw Error(msg.str());
+ }
+
+ process(isDryRun, filename);
+}
+
+void
+ConfigFile::process(bool isDryRun, const char* filename)
+{
+ // NFD_LOG_DEBUG("processing..." << ((isDryRun)?("dry run"):("")));
+
+ if (m_global.begin() == m_global.end())
+ {
+ std::string msg = "Error processing configuration file";
+ if (filename != 0)
+ {
+ msg += ": ";
+ msg += filename;
+ }
+ msg += " no data";
+ throw Error(msg);
+ }
+
+ for (ConfigSection::const_iterator i = m_global.begin(); i != m_global.end(); ++i)
+ {
+ const std::string& sectionName = i->first;
+ const ConfigSection& section = i->second;
+
+ SubscriptionTable::iterator subscriberIt = m_subscriptions.find(sectionName);
+ if (subscriberIt != m_subscriptions.end())
+ {
+ OnConfig subscriber = subscriberIt->second;
+ subscriber(section, isDryRun);
+ }
+ else
+ {
+ std::string msg = "Error processing configuration file";
+ if (filename != 0)
+ {
+ msg += " ";
+ msg += filename;
+ }
+ msg += " no module subscribed for section: " + sectionName;
+ throw Error(msg);
+ }
+ }
+}
+
+}
+
diff --git a/daemon/mgmt/config-file.hpp b/daemon/mgmt/config-file.hpp
new file mode 100644
index 0000000..2b72e7f
--- /dev/null
+++ b/daemon/mgmt/config-file.hpp
@@ -0,0 +1,89 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+#ifndef NFD_MGMT_CONFIG_FILE_HPP
+#define NFD_MGMT_CONFIG_FILE_HPP
+
+#include "common.hpp"
+
+#include <boost/property_tree/ptree.hpp>
+
+namespace nfd {
+
+typedef boost::property_tree::ptree ConfigSection;
+
+/// \brief callback for config file sections
+typedef function<void(const ConfigSection&, bool)> OnConfig;
+
+class ConfigFile
+{
+public:
+
+ class Error : public std::runtime_error
+ {
+ public:
+ Error(const std::string& what)
+ : std::runtime_error(what)
+ {
+
+ }
+ };
+
+ ConfigFile();
+
+ /// \brief setup notification of configuration file sections
+ void
+ addSectionHandler(const std::string& sectionName,
+ OnConfig subscriber);
+
+
+ /**
+ * \param filename file to parse
+ * \param isDryRun true if performing a dry run of configuration, false otherwise
+ * \throws ConfigFile::Error if file not found
+ * \throws ConfigFile::Error if parse error
+ */
+ void
+ parse(const char* filename, bool isDryRun=false);
+
+ /**
+ * \param input configuration (as a string) to parse
+ * \param isDryRun true if performing a dry run of configuration, false otherwise
+ * \param filename optional convenience argument to provide more detailed error messages (if available)
+ * \throws ConfigFile::Error if file not found
+ * \throws ConfigFile::Error if parse error
+ */
+ void
+ parse(const std::string& input, bool isDryRun=false, const char* filename=0);
+
+ /**
+ * \param input stream to parse
+ * \param isDryRun true if performing a dry run of configuration, false otherwise
+ * \param filename optional convenience argument to provide more detailed error messages (if available)
+ * \throws ConfigFile::Error if parse error
+ */
+ void
+ parse(std::istream& input, bool isDryRun=false, const char* filename=0);
+
+private:
+
+ void
+ process(bool isDryRun, const char* filename);
+
+private:
+
+ typedef std::map<std::string, OnConfig> SubscriptionTable;
+
+ SubscriptionTable m_subscriptions;
+
+ ConfigSection m_global;
+};
+
+} // namespace nfd
+
+
+#endif // NFD_MGMT_CONFIG_FILE_HPP
+
diff --git a/tests/mgmt/config-file.cpp b/tests/mgmt/config-file.cpp
new file mode 100644
index 0000000..bf275e4
--- /dev/null
+++ b/tests/mgmt/config-file.cpp
@@ -0,0 +1,347 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (C) 2014 Named Data Networking Project
+ * See COPYING for copyright and distribution information.
+ */
+
+
+#include "mgmt/config-file.hpp"
+
+#include <boost/test/unit_test.hpp>
+
+namespace nfd {
+
+NFD_LOG_INIT("ConfigFileTest");
+
+BOOST_AUTO_TEST_SUITE(MgmtConfigFile)
+
+// a
+// {
+// akey "avalue"
+// }
+// b
+// {
+// bkey "bvalue"
+// }
+
+const std::string CONFIG =
+"a\n"
+"{\n"
+" akey \"avalue\"\n"
+"}\n"
+"b\n"
+"{\n"
+" bkey \"bvalue\"\n"
+"}\n";
+
+
+// a
+// {
+// akey "avalue"
+// }
+// b
+//
+// bkey "bvalue"
+// }
+
+const std::string MALFORMED_CONFIG =
+"a\n"
+"{\n"
+" akey \"avalue\"\n"
+"}\n"
+"b\n"
+"\n"
+" bkey \"bvalue\"\n"
+"}\n";
+
+// counts of the respective section counts in config_example.info
+
+const int CONFIG_N_A_SECTIONS = 1;
+const int CONFIG_N_B_SECTIONS = 1;
+
+class DummySubscriber
+{
+public:
+
+ DummySubscriber(ConfigFile& config,
+ int nASections,
+ int nBSections,
+ bool expectDryRun)
+ : m_nASections(nASections),
+ m_nBSections(nBSections),
+ m_nRemainingACallbacks(nASections),
+ m_nRemainingBCallbacks(nBSections),
+ m_expectDryRun(expectDryRun)
+ {
+
+ }
+
+ void
+ onA(const ConfigSection& section, bool isDryRun)
+ {
+ // NFD_LOG_DEBUG("a");
+ BOOST_CHECK_EQUAL(isDryRun, m_expectDryRun);
+ --m_nRemainingACallbacks;
+ }
+
+
+ void
+ onB(const ConfigSection& section, bool isDryRun)
+ {
+ // NFD_LOG_DEBUG("b");
+ BOOST_CHECK_EQUAL(isDryRun, m_expectDryRun);
+ --m_nRemainingBCallbacks;
+ }
+
+ bool
+ allCallbacksFired() const
+ {
+ return m_nRemainingACallbacks == 0 &&
+ m_nRemainingBCallbacks == 0;
+ }
+
+ bool
+ noCallbacksFired() const
+ {
+ return m_nRemainingACallbacks == m_nASections &&
+ m_nRemainingBCallbacks == m_nBSections;
+ }
+
+ virtual
+ ~DummySubscriber()
+ {
+
+ }
+
+private:
+ int m_nASections;
+ int m_nBSections;
+ int m_nRemainingACallbacks;
+ int m_nRemainingBCallbacks;
+ bool m_expectDryRun;
+};
+
+class DummyAllSubscriber : public DummySubscriber
+{
+public:
+ DummyAllSubscriber(ConfigFile& config, bool expectDryRun=false)
+ : DummySubscriber(config,
+ CONFIG_N_A_SECTIONS,
+ CONFIG_N_B_SECTIONS,
+ expectDryRun)
+ {
+ config.addSectionHandler("a", bind(&DummySubscriber::onA, this, _1, _2));
+ config.addSectionHandler("b", bind(&DummySubscriber::onB, this, _1, _2));
+ }
+
+ virtual
+ ~DummyAllSubscriber()
+ {
+
+ }
+};
+
+class DummyOneSubscriber : public DummySubscriber
+{
+public:
+ DummyOneSubscriber(ConfigFile& config,
+ const std::string& sectionName,
+ bool expectDryRun=false)
+ : DummySubscriber(config,
+ (sectionName == "a"),
+ (sectionName == "b"),
+ expectDryRun)
+ {
+ if (sectionName == "a")
+ {
+ config.addSectionHandler(sectionName, bind(&DummySubscriber::onA, this, _1, _2));
+ }
+ else if (sectionName == "b")
+ {
+ config.addSectionHandler(sectionName, bind(&DummySubscriber::onB, this, _1, _2));
+ }
+ else
+ {
+ BOOST_FAIL("Test setup error: "
+ << "Unexpected section name "
+ <<"\"" << sectionName << "\"");
+ }
+
+ }
+
+ virtual
+ ~DummyOneSubscriber()
+ {
+
+ }
+};
+
+class DummyNoSubscriber : public DummySubscriber
+{
+public:
+ DummyNoSubscriber(ConfigFile& config, bool expectDryRun)
+ : DummySubscriber(config, 0, 0, expectDryRun)
+ {
+
+ }
+
+ virtual
+ ~DummyNoSubscriber()
+ {
+
+ }
+};
+
+BOOST_AUTO_TEST_CASE(OnConfigStream)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+ std::ifstream input;
+
+ input.open("tests/mgmt/config_example.info");
+ BOOST_REQUIRE(input.is_open());
+
+ file.parse(input);
+
+ BOOST_CHECK(sub.allCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigStreamEmptyStream)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ std::ifstream input;
+
+ BOOST_CHECK_THROW(file.parse(input), ConfigFile::Error);
+ BOOST_CHECK(sub.noCallbacksFired());
+}
+
+
+BOOST_AUTO_TEST_CASE(OnConfigString)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ file.parse(CONFIG);
+
+ BOOST_CHECK(sub.allCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigStringEmpty)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ BOOST_CHECK_THROW(file.parse(std::string()), ConfigFile::Error);
+ BOOST_CHECK(sub.noCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigStringMalformed)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ BOOST_CHECK_THROW(file.parse(MALFORMED_CONFIG), ConfigFile::Error);
+ BOOST_CHECK(sub.noCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigStringDryRun)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file, true);
+
+ file.parse(CONFIG, true);
+
+ BOOST_CHECK(sub.allCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigFilename)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ file.parse("tests/mgmt/config_example.info");
+
+ BOOST_CHECK(sub.allCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigFilenameNoFile)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ BOOST_CHECK_THROW(file.parse("i_made_this_up.info"), ConfigFile::Error);
+
+ BOOST_CHECK(sub.noCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigFilenameMalformed)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file);
+
+ BOOST_CHECK_THROW(file.parse("tests/mgmt/config_malformed.info"), ConfigFile::Error);
+
+ BOOST_CHECK(sub.noCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigStreamDryRun)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file, true);
+ std::ifstream input;
+
+ input.open("tests/mgmt/config_example.info");
+ BOOST_REQUIRE(input.is_open());
+
+ file.parse(input, true);
+
+ BOOST_CHECK(sub.allCallbacksFired());
+
+ input.close();
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigFilenameDryRun)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub(file, true);
+
+ file.parse("tests/mgmt/config_example.info", true);
+ BOOST_CHECK(sub.allCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigReplaceSubscriber)
+{
+ ConfigFile file;
+ DummyAllSubscriber sub1(file);
+ DummyAllSubscriber sub2(file);
+
+ file.parse(CONFIG);
+
+ BOOST_CHECK(sub1.noCallbacksFired());
+ BOOST_CHECK(sub2.allCallbacksFired());
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigUncoveredSections)
+{
+ ConfigFile file;
+
+ BOOST_CHECK_THROW(file.parse(CONFIG), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(OnConfigCoveredByPartialSubscribers)
+{
+ ConfigFile file;
+ DummyOneSubscriber subA(file, "a");
+ DummyOneSubscriber subB(file, "b");
+
+ file.parse(CONFIG);
+
+ BOOST_CHECK(subA.allCallbacksFired());
+ BOOST_CHECK(subB.allCallbacksFired());
+}
+
+} // namespace nfd
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/tests/mgmt/config_example.info b/tests/mgmt/config_example.info
new file mode 100644
index 0000000..d61691f
--- /dev/null
+++ b/tests/mgmt/config_example.info
@@ -0,0 +1,9 @@
+a
+{
+ akey "avalue"
+}
+
+b
+{
+ bkey "bvalue"
+}
\ No newline at end of file
diff --git a/tests/mgmt/config_malformed.info b/tests/mgmt/config_malformed.info
new file mode 100644
index 0000000..d3a1f9e
--- /dev/null
+++ b/tests/mgmt/config_malformed.info
@@ -0,0 +1,9 @@
+a
+{
+ akey "avalue"
+}
+
+b
+
+ bkey "bvalue"
+}
\ No newline at end of file