XML-based Configuration Files¶
The ConfigFile class provides an easy to use yet very powerful building block for complex applications. ConfigFile instances provide an intuitive interface for XML-based configuration files without the need for any manual XML-parsing. However, ConfigFile can be easily serialized into simple XML files that are still human readable.
The ConfigFile class provides “one line of code solutions” for:
serialization into XML-files
deserialization from XML-files
type-safe insertion of entries
type-safe retrieval of entries
static custom type registration mechanism
The XML Structure¶
First of all, let’s take a look at the XML-Structure that is used. The ConfigFile class uses a restricted XML-format using only three different types of tags:
a mandatory top level “<config>” tag
“section” tags with an “id” attribute: “<section id=’something’>”
“data” tags with an “id” and a “type” attribute: “<data id=’entry-id’ type=’int’>5</data>”
Here is an example xml-file:
<config>
<section id="general">
<data id="a vector" type="Vec">1 2 3 0</data>
<section id="params">
<data type="int" id="threshold">7</data>
<data type="double" id="value">6.450000</data>
<data type="string" id="filename">./notHallo.txt</data>
</section>
</section>
<section id="special">
<data type="char" id="hint">a</data>
<data type="char" id="no-hint">b</data>
</section>
</config>
As we can see, the file has a very simple structure. Sections may contain data nodes and/or other sections. Each data node defines an entry of the ConfigFile where the actual data entry is given by the text-value of the data nodes.
Reading and Writing XML Files¶
In C++, the file (here called config.xml can be loaded by:
ConfigFile cfg("config.xml");
This will parse the whole config-file and create an efficient STL-map based lookup for all it’s entries.
Note
Internally, we use the 3rd party library pugi-XML as XML parser, which is included as a source file and directly linked into the ICLUtils library
The ConfigFile instance cfg can now also intuitively be written to an xml-file (here named “config2.xml” using:
cfg.save("config2.xml");
Adding Entries to the XML File¶
Lets start with an empty file and add all the entries of the example above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <ICLUtils/ConfigFile.h>
#include <ICLQt/Common.h>
// for the Vec type
#include <ICLGeom/GeomDefs.h>
int main(){
ConfigFile cfg;
cfg["config.general.a vector"] = Vec(1,2,3,0);
cfg["config.general.params.threshold"] = 7;
cfg["config.general.params.value"] = 6.45;
cfg["config.general.params.filename"] = str("./notHallo.txt");
cfg["config.special.hint"] = 'a';
cfg["config.special.nohint"] = 'b';
SHOW(cfg);
}
|
As we can see, this is at least as transparent as writing the XML-file manually. Sections are addressed by the ‘.’-delimited string list where the last token always defines the actual data-entry’s id. The example also shows, that sections are automatically added on demand during the C++-based creation process. Furthermore, we demonstrate the automatic type inference mechanism, that derives the data-tag’s type entry from the source type. Types that are not registered cannot be added to ConfigFile instances:
struct MyType {};
ConfigFile f;
f["config.x"] = MyType(); // error type is not registered!
The Prefix¶
During the data insertion, we might have to use an identical prefix several times leading to many long lines in your C++-code. This can be bypassed by setting up the ConfigFile instance with a temporary path prefix.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <ICLUtils/ConfigFile.h>
#include <ICLQt/Common.h>
// for the Vec type
#include <ICLGeom/GeomDefs.h>
int main(){
ConfigFile cfg;
cfg["config.general.a vector"] = Vec(1,2,3,0);
cfg.setPrefix("config.general.params.");
cfg["threshold"] = 7;
cfg["value"] = 6.45;
cfg["filename"] = str("./notHallo.txt");
cfg.setPrefix("");
cfg["config.special.hint"] = 'a';
cfg["config.special.nohint"] = 'b';
SHOW(cfg);
}
|
Note
The prefix is just treated as an std::string variable, that is automatically prepended to the :icl::ConfigFile’s index operator’s argument. Therefore, the prefix must actually end with a ‘.’-character so that the following relative paths make sense then.
Extracting Entries for Config Files¶
For the extraction of ConfigFile entries, a special C++- technique is used in order to provide type-safe lvalue-based type inference. Let’s first consider some examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <ICLUtils/ConfigFile.h>
#include <ICLQt/Common.h>
// for the Vec type
#include <ICLGeom/GeomDefs.h>
int main(){
ConfigFile cfg("config-file.xml");
Vec v = cfg["config.general.a vector"];
cfg.setPrefix("config.general.params.");
int threshold = cfg["threshold"];
double value = cfg["value"];
std::string filename = cfg["filename"];
SHOW(v);
SHOW(threshold);
SHOW(value);
SHOW(filename);
}
|
The example demonstrates the intuitive mechanism. Please note, that extracting an entry as a wrong type leads to an exception, even if the assignment seems trivial:
float value2 = cfg["config.general.params.value"]; // error!
If a behavior like this is required, one can either first use a correct assignment:
double tmp = cfg["config.general.params.value"];
float value2 = tmp;
Or use the ConfigFile::Data::as method, that implements an explicit yet still type-safe cast into a given destination type:
float value2 = cfg["config.general.params.value"].as<double>();
Static Type Registration¶
The ConfigFile class can also be set up to accept custom types. For this, C++’s ostream- and istream-operators must fist be overloaded for the desired type. The new type can then be registered using the ConfigFile::register_type template, which gets a template-based type and a string-based type-ID. Here is an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <ICLUtils/ConfigFile.h>
#include <ICLQt/Common.h>
// a custom data type
struct MyPoint{
MyPoint(int x=0, int y=0):x(x),y(y){}
int x,y;
};
// overloaded ostream-operator
std::ostream &operator<<(std::ostream &s, const MyPoint &p){
return s << '(' << p.x << ',' << p.y << ')';
}
// overloaded istream-operator (note, this sould actually
// be more robust agains parsing errors)
std::istream &operator>>(std::istream &s, MyPoint &p){
char dummy;
return s >> dummy >> p.x >> dummy >> p.y >> dummy;
}
int main(){
ConfigFile cfg;
// register type
ConfigFile::register_type<MyPoint>("MyPoint");
// add type instance
cfg["config.p"] = MyPoint(1,2);
// extract type instance
MyPoint p = cfg["config.p"];
SHOW(p);
SHOW(cfg);
}
|
Use the ConfigFile for (De)serialization¶
For complex types it is usually quite difficult to find a nice way to implement C++-‘s istream- and ostream-operators. In particular the implementation if the istream-operator is usually very difficult because it should usually also detect parsing errors. Using the ConfigFile class for this has two advantages: first, it solves all serialization and deserialization issues, and second, it also leads to a human readable and well defined XML-based serialization string.
1 | THIS NEEDS TO BE WRITTEN
|