Chapter 2 The Configuration Class
- The ConfigurationException Class
- The create() and destroy() Operations
- Utility Operations
- The parse(), fileName() and empty() Operations
- Insertion and Removal Operations
- The lookup<Type>() Operations
- The type() and is<Type>() Operations
- The stringTo<Type>() Operations
- The List Operations
- Operations for Fallback Configuration
- Operations for Security Configuration
- Operations for the "uid-" Prefix
- The dump() Operation
2.1 The ConfigurationException Class
An exception of type ConfigurationException is thrown if any Config4Cpp operation fails. The public API of this class is shown in Figure 2.1.
// Access with #include <config4cpp/ConfigurationException.h> // or #include <config4cpp/Configuration.h> class ConfigurationException { public: ConfigurationException(const char * str); ConfigurationException(const ConfigurationException & other); ~ConfigurationException(); const char * c_str() const; };
Application code can access a string description of the exception by invoking the c_str() operation, as shown below:
try { ... } catch(const ConfigurationException & ex) { cerr << ex.c_str() << endl; }
As explained in Section 1.4, to avoid having a dependency on either classic or standard IO streams, Config4Cpp is not implemented with the IO Stream library. Because of this, Config4Cpp does not define an operator for streaming a ConfigurationException to an output stream. There is nothing preventing you from defining such a streaming operator in the global scope in your own applications. Alternatively, you can stream an exception to an output stream by explicitly invoking the c_str() operation, as shown in the previous example.
2.2 The create() and destroy() Operations
Figure 2.2 shows the operations that are used to create and destroy a Configuration object.
// Access with #include <config4cpp/Configuration.h> class Configuration { public: static Configuration * create(); virtual void destroy(); ... };
You use the static create() operation to create a Configuration object. A newly created Configuration object is empty initially. You can then populate it and access its contents, as I will discuss in the following sections of this chapter. Finally, you should call destroy() to reclaim the memory of the Configuration object.
The correct behaviour of Config4Cpp depends on the locale being set correctly. Because of this, it is advisable to call setlocale() before invoking any Config4Cpp APIs. If you do this, then Config4Cpp will be able to handle characters defined in your locale, such as European accented characters or Japanese ideographs. If you neglect to call setlocale(), then Config4Cpp is likely to correctly process only characters in the 7-bit US ASCII character set. Figure 2.3 illustrates how to call setlocale(), create() and destroy().
#include <locale.h> #include <config4cpp/Configuration.h> using namespace config4cpp; ... setlocale(LC_ALL, ""); Configuration * cfg = Configuration::create(); try { ... // invoke operations on cfg } catch(const ConfigurationException & ex) { cout << ex.c_str() << endl; } cfg->destroy();
Most of the operations defined in the Configuration class can throw ConfigurationException exceptions, so those operations should be called from inside a try-catch clause. However, the create() and destroy() operations do not throw that exception, so, as shown in Figure 2.3, they can be called from outside a try-catch clause.
2.3 Utility Operations
Config4Cpp provides several utility operations, shown in Figure 2.4, that you may need to use from time to time.
class Configuration { public: static void mergeNames(const char * scope, const char * localName, StringBuffer & fullyScopedName); static bool patternMatch(const char * str, const char * pattern); static int mbstrlen(const char * str); ... };
As I discussed in Section 1.6, many Config4* operations take a pair of parameters, scope and localName, that, when merged, specify the fully-scoped name of an entry in a Configuration object. The mergeNames() operation performs that merging, and it puts the result into the fullyScopedName parameter. The fully-scoped name is usually of the form scope.localName, but if either scope or localName is an empty string, then the dot (".") is omitted when performing the merge.
The patternMatch() operation compares a string against a pattern, and returns true if they match. Within the pattern, the "*" character acts as a wildcard that matches zero or more characters. For example:
Configuration::patternMatch("Hello, world", "Hello*") → true Configuration::patternMatch("Hello, world", "123*89") → false
Your locale setting specifies, amongst other things, the character set being used. If the locale specifies an 8-bit character set, such as ASCII or ISO-Latin-1, then each character is fully encoded in a single byte. However, if the locale specifies a multi-byte character set, such as UTF-8, then some characters may be encoded in a single byte but other characters will be encoded in a multi-byte sequence. Because of this possibility, you cannot rely on using the strlen() function to return the number of characters (instead of bytes) in a string. The mbstrlen() operation returns the number of characters in a string, regardless of the character set specified by the locale setting; it returns -1 if the string contains invalid multi-byte characters.
2.4 The parse(), fileName() and empty() Operations
Figure 2.5 shows the signatures of the fileName(), parse() and empty() operations.
class Configuration { public: enum SourceType {INPUT_FILE, INPUT_STRING, INPUT_EXEC}; const char * fileName() const; void parse(Configuration::SourceType sourceType, const char * source, const char * sourceDescription = "") throw(ConfigurationException); void parse(const char * sourceTypeAndSource) throw(ConfigurationException); void empty(); ... };
The fileName() operation returns the name of the most recently parsed file. If parse() has not previously been called, then fileName() returns an empty string.
I defer discussion of the one-parameter version of parse() until Section 2.4.4 because it is just a simplified version of the three-parameter version of parse(). In the three-parameter version of parse(), the value of the first parameter determines the meaning of the other parameters.
-
If the first parameter is INPUT_FILE, then the second
parameter is the name of the file to be parsed, and the third
parameter is ignored.
The second parameter will be the value returned from future calls to fileName().
-
If the first parameter is INPUT_STRING, then the second
parameter is a string to be parsed.
If the third parameter is not an empty string, then it will be the value returned from future calls to fileName(); otherwise the string "<string-based configuration>" will be the value returned from future calls to fileName().
-
If the first parameter is INPUT_EXEC, then the second
parameter is an external command to be executed and whose
standard output is to be parsed.
If the third parameter is not an empty string, then it will be the value returned from future calls to fileName(); otherwise the string resulting from appending the second parameter to "exec#" will be the value returned from future calls to fileName().
The string returned from fileName() is used at the start of text messages inside exceptions. For example, many components of Config4Cpp (including the parser, schema validator and lookup operations) format exception messages as shown below:
if (...) {
StringBuffer msg;
msg << cfg->fileName() << ": something went wrong";
throw ConfigurationException(msg.c_str());
}
For this reason, if you call parse() with INPUT_STRING for the first parameter, then you should ensure that the value of the third parameter acts as a descriptive “file name”.
2.4.1 Parsing a File
The code segment in Figure 2.6 shows an example use of parse(). The create() operation creates a Configuration object that is empty initially. Then the parse() operation is used to populate the Configuration object. A try-catch clause is used to print any exception that might be thrown. Once the Configuration object has been populated, lookup operations (which I will discuss in Section 2.6) can be used to access information in it. Finally, destroy() is called to reclaim the memory of the Configuration object when it is no longer required.
Configuration * cfg = Configuration::create(); try { cfg->parse(Configuration::INPUT_FILE, "myFile.cfg"); ... // invoke lookup operations } catch(const ConfigurationException & ex) { cerr << ex.c_str() << endl; } cfg->destroy();
2.4.2 Parsing the Output of a Command
The example in Figure 2.6 used the following to parse a file:
cfg->parse(Configuration::INPUT_FILE, "myFile.cfg");
If, instead of parsing a file, you want to execute a command and parse its standard output, then you can do so as follows:
cfg->parse(Configuration::INPUT_EXEC, "curl -sS http://host/file.cfg");
Using INPUT_EXEC for the first parameter tells parse() to to interpret the second parameter as the name of a command to be executed.
2.4.3 Parsing a String
The example in Figure 2.6 used the following to parse a file:
cfg->parse(Configuration::INPUT_FILE, "myFile.cfg");
If, instead of parsing a file, you want to parse a string, then you can do so as follows:
const char * cfgStr = ...; cfg->parse(Configuration::INPUT_STRING, cfgStr, "embedded configuration");
Using INPUT_STRING for the first parameter tells parse() to interpret the second parameter as configuration data that should be parsed directly, and the third parameter is the “file name” that will be used when reporting errors. You can initialise the second parameter in a variety of ways, for example:
- You could use the config2cpp utility to convert a configuration file into (a class wrapper around) a string that is embedded into the application. In this case, you might use "embedded configuration" or "fallback configuration" as the third parameter.
- Perhaps you are developing a client-server application that serialises messages into Config4* syntax and then transmits them across a socket connection. In the receiving application, the second parameter to parse() would be a message that was read from a socket connection. In this case, you might use "incoming message" as the third parameter.
2.4.4 The Simplified Version of parse()
The one-parameter version of parse() is a simplification wrapper around the three-parameter version. Its implementation is shown in Figure 2.7.
void Configuration::parse(const char * str) throw(ConfigurationException) { if (strncmp(str, "exec#", 5) == 0) { parse(Configuration::INPUT_EXEC, &(str[5])); } else if (strncmp(str, "file#", 5) == 0) { parse(Configuration::INPUT_FILE, &(str[5])); } else { parse(Configuration::INPUT_FILE, str); } }
The following examples show how to use this simplified version:
cfg->parse("exec#curl -sS http://host/file.cfg"); cfg->parse("file#file.cfg"); cfg->parse("file.cfg");
In practice, the parameter to this operation is unlikely to be hard-coded into an application, but rather will come from, say, a command-line option or an environment variable.
2.4.5 Parsing Multiple Files and the empty() Operation
If you want to parse multiple configuration files, then you can use multiple Configuration objects. Alternatively, you can reuse the same object multiple times. If you do this, then you will probably want to call empty() between successive calls of parse(), as shown in Figure 2.8. The empty() operation has the effect of removing all variables and scopes from the Configuration object.
Configuration * cfg = Configuration::create(); try { cfg->parse("file1.cfg"); ... // Access the configuration information cfg->empty(); cfg->parse("file2.cfg"); ... // Access the configuration information cfg->empty(); cfg->parse("file3.cfg"); ... // Access the configuration information cfg->empty(); cfg->parse("file4.cfg"); ... // Access the configuration information } catch(const ConfigurationException & ex) { cerr << ex.c_str() << endl; } cfg->destroy();
It is legal to call parse() multiple times without calling empty() between successive calls. If you do this, then each call to parse() merges its information with information already in the Configuration object. The Config4* parser implements the @include statement by (recursively) calling parse(), so you can think of multiple calls to parse() without calls to empty() as being similar to multiple @include statements.
It is difficult to think of a compelling reason why you might want to use a single Configuration object to parse multiple configuration files without calling empty() between successive calls of parse(). However, it is useful to know what the semantics of doing so are, because it can help you understand what is happening if you forget to call empty() between calls to parse() on the same Configuration object.
2.5 Insertion and Removal Operations
Most applications will populate a Configuration object by parsing a configuration file. However, it is possible to populate a Configuration object by using the operations shown in Figure 2.9.
class Configuration { public: void insertString( const char * scope, const char * localName, const char * strValue) throw(ConfigurationException); void insertList( const char * scope, const char * localName, const char ** array, int arraySize) throw(ConfigurationException); void insertList( const char * scope, const char * localName, const char ** nullTerminatedArray) throw(ConfigurationException); void insertList( const char * scope, const char * localName, const StringVector & vec) throw(ConfigurationException); void ensureScopeExists( const char * scope, const char * localName) throw(ConfigurationException); void remove(const char * scope, const char * localName) throw(ConfigurationException); ... };
The insertString() operation inserts into the Configuration object an entry using the fully-scoped name (obtained by merging the scope and localName parameters) and the specified strValue. If a variable of the same name already exists in the Configuration object, then it is replaced with the new value.
The insertList() operation inserts into the Configuration object an entry using the fully-scoped name (obtained by merging the scope and localName parameters) and the specified list. If a variable of the same name already exists in the Configuration object then it is replaced with the new value. The insertList() operation is overloaded so you can specify the list in one of three different ways: as an array of strings plus the size of the array, as an array of strings terminated by a null pointer, or as a StringVector.
The insertString() and insertList() operations make a deep copy of the value when inserting it into the Configuration object.
The ensureScopeExists() operation merges the scope and localName parameters to obtain a fully-scoped name. It ensures that a scope with this fully-scoped name exists. If any ancestors of the specified scopes are missing, then they are also created. Internally, the insertString() and insertList() operations call ensureScopeExists(). Because of this, applications rarely need to call ensureScopeExists() directly.
The remove() operation merges scope and localName to form a fully-scoped name. It then removes the entry with the specified name.
If you are making use of identifiers that have "uid-" prefixes, then it is your duty to expand such identifiers before invoking any of the operations listed in Figure 2.9. Section 2.12 explains how to expand identifiers that have "uid-" prefixes.
2.6 The lookup<Type>() Operations
Figure 2.10 lists lookup-style operations that you can use to access the values of configuration variables. There are a lot of lookup operations, for the following reasons.
- Syntactically, variables in a configuration file are either strings or lists. However, strings are often used to encode other types, such as integers, floats, booleans, durations and so on. Because of this, there are type-safe lookup operations that convert a string value to another format. For example, lookupInt() converts a string value to an int, and lookupBoolean() converts a string value to a bool.
- The lookup operations are overloaded so that you can optionally specify a default value that should be returned if the specified configuration variable is not present.
- The lookupList() operation is overloaded so you can access a list as an array of strings or a StringVector.
struct EnumNameAndValue { const char * name; int value; }; class Configuration { public: const char * lookupString( const char * scope, const char * localName, const char * defaultVal) const throw(ConfigurationException); const char * lookupString( const char * scope, const char * localName) const throw(ConfigurationException); void lookupList( const char * scope, const char * localName, const char **& array, int & arraySize, const char ** defaultArray, int defaultArraySize) const throw(ConfigurationException); void lookupList( const char * scope, const char * localName, const char **& array, int & arraySize) const throw(ConfigurationException); void lookupList( const char * scope, const char * localName, StringVector & list, const StringVector & defaultList) const throw(ConfigurationException); void lookupList( const char * scope, const char * localName, StringVector & list) const throw(ConfigurationException); int lookupInt( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupInt( const char * scope, const char * localName) const throw(ConfigurationException); float lookupFloat( const char * scope, const char * localName, float defaultVal) const throw(ConfigurationException); float lookupFloat( const char * scope, const char * localName) const throw(ConfigurationException); int lookupEnum( const char * scope, const char * localName, const char * typeName, const EnumNameAndValue * enumInfo, int numEnums, const char * defaultVal) const throw(ConfigurationException); int lookupEnum( const char * scope, const char * localName, const char * typeName, const EnumNameAndValue * enumInfo, int numEnums, int defaultVal) const throw(ConfigurationException); int lookupEnum( const char * scope, const char * localName, const char * typeName, const EnumNameAndValue * enumInfo, int numEnums) const throw(ConfigurationException); bool lookupBoolean( const char * scope, const char * localName, bool defaultVal) const throw(ConfigurationException); bool lookupBoolean( const char * scope, const char * localName) const throw(ConfigurationException); void lookupFloatWithUnits( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, float & floatResult, const char *& unitsResult) const throw(ConfigurationException); void lookupFloatWithUnits( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, float & floatResult, const char *& unitsResult, float defaultFloat, const char * defaultUnits) const throw(ConfigurationException); void lookupUnitsWithFloat( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, float & floatResult, const char *& unitsResult) const throw(ConfigurationException); void lookupUnitsWithFloat( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, float & floatResult, const char *& unitsResult, float defaultFloat, const char * defaultUnits) const throw(ConfigurationException); void lookupIntWithUnits( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, int & intResult, const char *& unitsResult) const throw(ConfigurationException); void lookupIntWithUnits( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, int & intResult, const char *& unitsResult, int defaultInt, const char * defaultUnits) const throw(ConfigurationException); void lookupUnitsWithInt( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, int & intResult, const char *& unitsResult) const throw(ConfigurationException); void lookupUnitsWithInt( const char * scope, const char * localName, const char * typeName, const char ** allowedUnits, int allowedUnitsSize, int & intResult, const char *& unitsResult, int defaultInt, const char * defaultUnits) const throw(ConfigurationException); int lookupDurationMicroseconds( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupDurationMicroseconds( const char * scope, const char * localName) const throw(ConfigurationException); int lookupDurationMilliseconds( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupDurationMilliseconds( const char * scope, const char * localName) const throw(ConfigurationException); int lookupDurationSeconds( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupDurationSeconds( const char * scope, const char * localName) const throw(ConfigurationException); int lookupMemorySizeBytes( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupMemorySizeBytes( const char * scope, const char * localName) const throw(ConfigurationException); int lookupMemorySizeKB( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupMemorySizeKB( const char * scope, const char * localName) const throw(ConfigurationException); int lookupMemorySizeMB( const char * scope, const char * localName, int defaultVal) const throw(ConfigurationException); int lookupMemorySizeMB( const char * scope, const char * localName) const throw(ConfigurationException); void lookupScope( const char * scope, const char * localName) const throw(ConfigurationException); };
The lookup operations perform error checking. For example, if the lookupInt() operation cannot convert the string value into an integer, then it throws an exception that contains an easy-to-understand error message. Likewise, if you do not specify a default value to a lookup operation and the specified configuration variable is missing (from both the main configuration object and fallback configuration), then an exception is thrown.
2.6.1 Lookup Operations for Enumerated Types
Among other parameters, the lookupEnum() operation takes an array of EnumNameAndValue structures and the size of that array. This operation calls lookupString() and then uses information in the array to convert the string value into an integer. For example:
EnumNameAndValue colourInfo[] = {
{ "red", 0 },
{ "green", 1 },
{ "blue", 2 },
};
colour = cfg->lookupEnum(scope, "font_colour", "colour", colourInfo, 3);
The typeName parameter ("colour" in the above example) specifies the “type name” of the enum names, and is used to construct an informative error message if an exception is thrown.
2.6.2 Lookup Operations for Unit-based Types
Lookup operations that have Units in their name take, among other parameters, an array of strings (specifying the allowed units) and the size of that array. An example can be seen in Figure 2.11.
float amount; const char * currency; const char * currencies[] = {"£", "$", "€"}; cfg->lookupUnitsWithFloat(scope, "discount_price", "price", currencies, 3, amount, currency);
The typeName parameter ("price" in the example) specifies the “type name” of correctly-formatted strings, and is used to construct an informative error message if an exception is thrown.
The output parameters, amount and currency, contain the quantity and units that were parsed from the value of the configuration variable.
2.7 The type() and is<Type>() Operations
Figure 2.12 shows the operations you can use to query type information.
struct EnumNameAndValue { const char * name; int value; }; class Configuration { public: enum Type {CFG_NO_VALUE = 0, // bit masks CFG_STRING = 1, // 0001 CFG_LIST = 2, // 0010 CFG_SCOPE = 4, // 0100 CFG_VARIABLES = 3, // 0011 = STRING | LIST CFG_SCOPE_AND_VARS = 7 // 0111 = STRING | LIST | SCOPE }; Type type(const char * scope, const char * localName) const; bool isBoolean(const char * str) const; bool isInt(const char * str) const; bool isFloat(const char * str) const; bool isDurationMicroseconds(const char * str) const; bool isDurationMilliseconds(const char * str) const; bool isDurationSeconds(const char * str) const; bool isMemorySizeBytes(const char * str) const; bool isMemorySizeKB(const char * str) const; bool isMemorySizeMB(const char * str) const; bool isEnum(const char * str, const EnumNameAndValue * enumInfo, int numEnums) const; bool isFloatWithUnits( const char * str, const char ** allowedUnits, int allowedUnitsSize) const; bool isIntWithUnits( const char * str, const char ** allowedUnits, int allowedUnitsSize) const; bool isUnitsWithFloat( const char * str, const char ** allowedUnits, int allowedUnitsSize) const; bool isUnitsWithInt( const char * str, const char ** allowedUnits, int allowedUnitsSize) const; ... };
The type() operation merges the scope and localName parameters to form the fully-scoped name of an entry in the Configuration object, and then returns the type of that entry. The return value of this operation will be one of the following:
Return value Meaning Configuration::CFG_NO_VALUE The entry does not exist Configuration::CFG_STRING The entry is a string variable Configuration::CFG_LIST The entry is a list variable Configuration::CFG_SCOPE The entry is a scope
Operations with names of the form is<Type>() return true if the str parameter is of the specified type. For example:
cfg->isBoolean("true") → true cfg->isBoolean("Fred") → false cfg->isDurationSeconds("2.5 minutes") → true cfg->isDurationSeconds("100 milliseconds") → false
The isEnum() operation takes three parameters: a string to be tested, an array of EnumNameAndValue structures and the size of that array. For example:
EnumNameAndValue colourInfo[] = { { "red", 0 }, { "green", 1 }, { "blue", 2 }, }; cfg->isEnum("red", colourInfo, 3) → true cfg->isEnum("foo", colourInfo, 3) → false
The is<Type>() operations with "Units" in their name take three parameters: a string to be tested, an array of strings (specifying the allowed units) and the size of that array. For example:
const char * currencies[] = {"£", "$", "€"}; cfg->isUnitsWithFloat("£19.99", currencies, 3) → true cfg->isUnitsWithFloat("foobar", currencies, 3) → false
2.8 The stringTo<Type>() Operations
Figure 2.13 lists operations that can convert a string value to another type.
class Configuration { public: bool stringToBoolean( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToInt( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); float stringToFloat( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToDurationSeconds( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToDurationMilliseconds( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToDurationMicroseconds( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToMemorySizeBytes( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToMemorySizeKB( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToMemorySizeMB( const char * scope, const char * localName, const char * str) const throw(ConfigurationException); int stringToEnum( const char * scope, const char * localName, const char * typeName, const char * str, const EnumNameAndValue * enumInfo, int numEnums) const throw(ConfigurationException); void stringToFloatWithUnits( const char * scope, const char * localName, const char * typeName, const char * str, const char ** allowedUnits, int allowedUnitsSize, float & floatResult, const char *& unitsResult) const throw(ConfigurationException); void stringToUnitsWithFloat( const char * scope, const char * localName, const char * typeName, const char * str, const char ** allowedUnits, int allowedUnitsSize, float & floatResult, const char *& unitsResult) const throw(ConfigurationException); void stringToIntWithUnits( const char * scope, const char * localName, const char * typeName, const char * str, const char ** allowedUnits, int allowedUnitsSize, int & intResult, const char *& unitsResult) const throw(ConfigurationException); void stringToUnitsWithInt( const char * scope, const char * localName, const char * typeName, const char * str, const char ** allowedUnits, int allowedUnitsSize, int & intResult, const char *& unitsResult) const throw(ConfigurationException); ... };
An operation with a name of the form stringTo<Type>() converts a string into the specified type. If the conversion fails, then the operation throws an exception containing an informative error message. The error message will indicate that the problem arose with the variable identified by the fully-scoped name (obtained by merging the scope and localName parameters) in the fileName() configuration file.
As an example, consider a call to stringToInt() in which the scope parameter is "foo", the localName parameter is "my_list[3]" and the str parameter is "Hello, world". If the configuration file previously parsed was called example.cfg, then the message in the exception will be:
example.cfg: Non-integer value for ’foo.my_list[3]’
The intention is that developers will iterate over all the strings within a list and handcraft the localName parameter for each list element to reflect its position within the list: "my_list[1]", "my_list[2]", "my_list[3]" and so on. In this way, the stringTo<Type>() operations can produce informative exception messages if a data-type conversion fails. Note that although many programming languages, including C++, index arrays starting from 0, you should format the localName parameter so the index starts at 1. This is to be consistent with the error messages produced by the SchemaValidator class.
2.9 The List Operations
Figure 2.14 shows the operations for listing the names of entries within a scope. There are two list-type operations: listFullyScopedNames() and listLocallyScopedNames(). However, there are three overloaded versions of each operation, thus making for six variants in total.
class Configuration { public: enum Type {CFG_NO_VALUE = 0, // bit masks CFG_STRING = 1, // 0001 CFG_LIST = 2, // 0010 CFG_SCOPE = 4, // 0100 CFG_VARIABLES = 3, // 0011 = STRING | LIST CFG_SCOPE_AND_VARS = 7 // 0111 = STRING | LIST | SCOPE }; void listFullyScopedNames( const char * scope, const char * localName, Type typeMask, bool recursive, StringVector & names) const throw(ConfigurationException); void listFullyScopedNames( const char * scope, const char * localName, Type typeMask, bool recursive, const char * filterPattern, StringVector & names) const throw(ConfigurationException); void listFullyScopedNames( const char * scope, const char * localName, Type typeMask, bool recursive, const StringVector & filterPatterns, StringVector & names) const throw(ConfigurationException); void listLocallyScopedNames( const char * scope, const char * localName, Type typeMask, bool recursive, StringVector & names) const throw(ConfigurationException); void listLocallyScopedNames( const char * scope, const char * localName, Type typeMask, bool recursive, const char * filterPattern, StringVector & names) const throw(ConfigurationException); void listLocallyScopedNames( const char * scope, const char * localName, Type typeMask, bool recursive, const StringVector & filterPatterns, StringVector & names) const throw(ConfigurationException); ... };
The list operations merge the scope and localName parameters to form the fully-scoped name of a scope, and populate the output names parameter with a sorted list of the names of entries in that scope. The boolean recursive parameter specifies whether the list operation should recurse into nested sub-scopes. The typeMask parameter is a bit mask that specifies which types of entries should be listed. For example, specifying CFG_VARIABLES will list only the names of variables, while CFG_SCOPE will list only the names of scopes.
By default, a list operation lists the names of all the specified entries. However, if one or more filter patterns are specified, then the list operation will use the patternMatch() operation (Section 2.3) to compare each name against the specified patterns, and only names that match at least one filter pattern will be included in the list results.
As an example of the list functions, consider the configuration file shown below:
foo { timeout = "2 minutes"; log { level = "2"; file = "/tmp/foo.log"; }; }
The following invocation of listFullyScopedNames():
cfg->listFullyScopedNames("foo", "", Configuration::CFG_SCOPE_AND_VARS, true, names);
results in names containing the following strings:
"foo.log" "foo.log.level" "foo.log.file" "foo.timeout"
If the same parameters are passed to listLocallyScopedNames(), then names will contain similar strings, but each string will be missing the "foo." prefix.
If you intend to make use of filter patterns, then you should note that filter patterns are matched against the strings that are produced by the list operation. For example, the filter pattern "time*" matches against "timeout", which is produced by listLocallyScopedNames() in the previous example, but it does not match against "foo.timeout", which is produced by listFullyScopedNames(). Because of this, you should use mergeNames() (Section 2.3) to prefix filter patterns with the name of the scope being listed when using listFullyScopedNames(). This is illustrated in the following example:
StringBuffer filterPattern; Configuration::mergeNames(scope, "time*", filterPattern); cfg->listFullyScopedNames(scope, "", Configuration::CFG_SCOPE_AND_VARS, true, filterPattern, names);
The list operations call unexpandUid()—discussed in Section 2.12—on a name before comparing it against filter patterns. Because of this, filter patterns can work with names that have an "uid-" prefix. For example, the code below obtains a list of the names of all uid-recipe scopes:
StringBuffer filterPattern; Configuration::mergeNames(scope, "uid-recipe", filterPattern); cfg->listFullyScopedNames(scope, "", Configuration::CFG_SCOPE, true, filterPattern, names);
2.10 Operations for Fallback Configuration
The operations for getting and setting fallback configuration are shown in Figure 2.15.
class Configuration { public: void setFallbackConfiguration(Configuration * cfg); void setFallbackConfiguration( Configuration::SourceType sourceType, const char * source, const char * sourceDescription = "") throw(ConfigurationException); const Configuration * getFallbackConfiguration(); ... };
The one-parameter version of setFallbackConfiguration() requires you to create and populate the fallback configuration object yourself. The three-parameter version creates an initially empty fallback configuration object and then populates it by calling parse() with the specified parameters.
A “main” configuration object takes ownership of its fallback configuration object. Because of this, when you invoke destroy() on the “main” configuration object, its fallback configuration object is also destroyed.
2.11 Operations for Security Configuration
As explained in the Config4* Security chapter of the Config4* Getting Started Guide, Config4* has a built-in security policy that is applied to all Configuration objects by default. You can query the current security policy of a Configuration object by calling getSecurityConfiguration(), which is shown in Figure 2.16.
class Configuration { public: void setSecurityConfiguration( Configuration * cfg, bool takeOwnership, const char * scope = "") throw(ConfigurationException); void setSecurityConfiguration( const char * cfgInput, const char * scope = "") throw(ConfigurationException); void getSecurityConfiguration( const Configuration *& cfg, const char *& scope); ... };
You can change the security policy of an individual Configuration object by calling setSecurityConfiguration(). This operation is overloaded. The first variant shown in Figure 2.16 enables you to use an existing Configuration object as a security policy. If takeOwnership is true, then the target Configuration object will “take ownership” of the security policy object, which means the security policy object will be destroyed when the target Configuration object is destroyed.
The second variant of setSecurityConfiguration enables you to specify a file or "exec#..." that should be parsed to obtain a security policy. The target Configuration object will “take ownership” of the security policy object.
The security policy is specified by the combination of three variables (allow_patterns, deny_patterns and trusted_directories) in the specified scope of the security policy object. See the Config4* Security chapter of the Config4* Getting Started Guide for details.
2.12 Operations for the "uid-" Prefix
An identifier that has an "uid-" prefix has both an expanded and unexpanded form. For example, uid-000000042-recipe is an identifier in its expanded form, while uid-recipe is its unexpanded counterpart. Figure 2.17 lists the operations that Config4Cpp provides for manipulating expanded and unexpanded uid identifiers.
class Configuration { public: void expandUid(StringBuffer & spelling) throw(ConfigurationException); const char * unexpandUid( const char * spelling, StringBuffer & buf) const; bool uidEquals(const char * s1, const char * s2) const; ... };
Each Configuration object keeps an internal counter that starts at 0 and is incremented every time expandUid() encounters an "uid-" prefix. The current value of that counter is used by expandUid() to replace an identifier with its expanded form.
If you populate a Configuration object by calling parse(), then you are unlikely to need to call expandUid(), because the parser invokes that operation automatically whenever it encounters an identifier with an "uid-" prefix.
However, if you populate a Configuration object by invoking the insertion operations discussed in Section 2.5, then it is your responsibility to expand identifiers before invoking the insertion operations.
The uidEquals() operation calls unexpandUid() for both of its parameters, and tests the resulting names for equality. For example:
cfg->uidEquals("uid-000000042-recipe", "uid-recipe") → true cfg->uidEquals("uid-000000042-recipe", "uid-employee") → false
2.13 The dump() Operation
When Config4Cpp parses a configuration file, it stores information about scopes and name=value pairs in hash tables. Config4Cpp provides a dump() operation, shown in Figure 2.18, that converts information in the hash tables into the syntax of a Config4* file; this result is stored in the buf parameter.
class Configuration { public: void dump(StringBuffer & buf, bool wantExpandedUidNames, const char * scope, const char * localName) const throw(ConfigurationException); void dump(StringBuffer & buf, bool wantExpandedUidNames) const; ... };
The dump() operation is overloaded. The version that takes scope and localName parameters merges those parameters to form the fully-scoped name of an entry, and then provides a dump of that entry. This version of dump() will throw an exception if the fully-scoped name is of an non-existent entry.
The other version of the operation dumps the entire Configuration object.
Both versions of the dump() operation take a boolean parameter, wantExpandedUidNames, that specifies whether entries that have an "uid-" prefix should have their names dumped in expanded or unexpanded form.