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:

  1. a mandatory top level “<config>” tag

  2. “section” tags with an “id” attribute: “<section id=’something’>”

  3. “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