Previous Up Next

Chapter 11  Demonstration Applications

11.1  Introduction

Several demonstration applications are supplied with implementations of Config4*. You can find these in the demo directory hierarchy of your distribution. I advise readers to examine the source code of these demos while reading the discussion of the demos in this chapter.

11.2  The simple-encapsulation Demo

The simple-encapsulation demo defines a class that provides the configuration capabilities for a hypothetical Foo application. This application requires only seven configuration variables so, rather than expose general purpose lookup-style operations, the FooConfiguration class provides accessor operations for these seven pieces of configuration. The class surrounds calls to Config4* with a try-catch clause so the information inside a Config4* exception can be copied to an application-specific exception that is thrown. In these simple ways, the FooConfiguration class encapsulates the use of Config4*, thus making it easy for a maintainer of the Foo application to migrate to using a different configuration technology, should the need ever arise.

Despite FooConfiguration having a simple-to-use API, the class encapsulates some powerful features of Config4*. First, the class allows the default security policy to be overridden (Section 5.4). Second, the class uses fallback configuration (Section 3.6.3) so users can run the Foo application without a configuration file. The source used for the fallback configuration is an embedded string that the Makefile or build.xml file generates by use of the config2cpp or config2j utility.

The FooConfiguration::parse() operation takes four string parameters that specify the configuration source (a file or "exec#...") and scope within it, and a security source and scope within it. Empty strings can be passed for any or all of these parameters, thus making a user-specified configuration source and user-specified security policy optional.

If FooConfiguration were re-implemented to use, say, an XML file or a Java properties file then the four parameters to parse() would likely be replaced by a single parameter denoting the name of the XML file or Java properties file. This change in the public API of FooConfiguration is likely to have minimal knock-on effects in the rest of the Foo application. In particular, the parsing of command-line arguments and subsequent creation of a FooConfiguration object are the only pieces of code likely to be affected.

11.3  The encapsulate-lookup-api Demo

The encapsulate-lookup-api demo defines a class that provides the configuration capabilities for a hypothetical Foo application. This application requires general purpose lookup-style operations when accessing configuration information. The FooConfiguration class provides its own lookup-style operations that internally delegate to the lookup operations of Config4*. In this way, the FooConfiguration class encapsulates the use of Config4*, thus making it easy for a maintainer of the Foo application to migrate to using a different configuration technology, should the need ever arise.

The delegation logic is straightforward but a bit verbose due to the need to surround calls to Config4* with a try-catch clause so the information inside a Config4* exception can be copied to an application-specific exception that is thrown. This type of delegation logic occupies only a few lines of code for each lookup-style operation. The verbosity arises because Config4* provides so many lookup-style operations. Config4* has lookup operations for many different types, and for each type the lookup operation is overloaded to provide a “lookup with a default value” and a “lookup without a default value.” To keep the volume of code manageable, the demo does not provide the “lookup with a default value” version of operations. Instead, it uses fallback configuration (Section 3.6.3) to provide default values.

11.4  The log-level Demo

Many applications have the ability to print diagnostic messages (as a troubleshooting aid when something goes wrong), and use a command-line option or variable in a configuration file to set the diagnostics level. A primitive way to control the diagnostics level is to have a, say, a "-d <int>" command-line option that sets the diagnostics level for the entire application. However, this simplistic approach can result in “too many” irrelevant diagnostics messages being printed, which can hinder attempts to diagnose a problem.

A better approach is to provide an independent diagnostic level for each component in an application. This makes it possible to selectively turn on diagnostics for some components, while turning off diagnostics for other components. This flexible approach is becoming increasingly common, though the granularity of a “component” varies widely. In some applications, a component might be coarse-grained, such as an entire functional subsystem or a plug-in. In other applications, a component might be finer-grained, such as individual classes (which is common in Log4J-based applications), or even individual operations on a class.

If you want to use this “separate log-level for each component” technique in a Config4*-based application, then you might think of using a separate configuration variable for each component. For example:

log_level {
    component1 = "2";
    component2 = "0";
    component3 = "0";
};

However, that approach does not scale well: if you have hundreds of components in your application, then you will need hundreds of configuration variables to control their log levels.

My preferred approach is to use a two-column table that provides a mapping from the wildcarded name of a component to a log level. You can see an example of this in Figure 11.1. The string "A::op3" denotes operation op3() in the class A.1 The wildcard character ("*") matches zero or more characters, and it might be used to match, say, the names of all operations within a specific class ("B::*"), all create-style operations, regardless of the class in which they appear ("*::create*"), or all operations in all classes ("*").

Figure 11.1: Using a table to specify log levels
log_level = [
    # wildcarded component name   log level
    #--------------------------------------
    "A::op3",                     "4",
    "B::op1",                     "3",
    "B::*",                       "1",
    "*",                          "0",
];

When a component needs to determine its log level, it iterates through the rows of the table, and uses the log level of the first matching wildcarded entry.2 Thus, the last line of the table can specify a default log level (by using "*" as the wildcarded component name), and earlier lines in the table can specify a different log level for individual components (or groups of components). This combination of wildcarding and defaulting means that the table can remain short even if an application contains hundreds or thousands of components.

The log-level demo provides code that illustrates how to use a table to specify wildcarded log levels.

11.5  The recipes Demo

The recipes demo provides an example of how to process scopes and variables that contain the "uid-" prefix. The recipes.cfg file contains a collection of uid-recipe scopes, like those shown in Figure 11.2.

Figure 11.2: File of recipes
uid-recipe {
    name = "Tea";
    ingredients = ["1 tea bag", "cold water", "milk"];
    uid-step = "Pour cold water into the kettle";
    uid-step = "Turn on the kettle";
    uid-step = "Wait for the kettle to boil";
    uid-step = "Pour boiled water into a cup";
    uid-step = "Add tea bag to cup & leave for 3 minutes";
    uid-step = "Remove tea bag";
    uid-step = "Add a splash of milk if you want";
}
uid-recipe {
    name = "Toast";
    ingredients = ["Two slices of bread", "butter"];
    uid-step = "Place bread in a toaster and turn on";
    uid-step = "Wait for toaster to pop out the bread";
    uid-step = "Remove bread from toaster and butter it";
}

The RecipeFileParser class provides a simple API for parsing such a file and iterating over the recipes contained within it. The parse() operation within this class illustrates how to perform schema validation for scopes and variables that contain the "uid-" prefix. In addition, the parse() and getRecipeSteps() operations illustrate how to use pass a filter string such as "uid-recipe" or "uid-step" to listFullyScopedNames() and listLocallyScopedNames() to get a list of "uid-" entries.

11.6  The extended-schema-validator Demo

This demo application illustrates how to enhance the Config4* schema validator with knowledge of additional schema types.

The ExtendedSchemaValidator class illustrates how to write a subclass of SchemaValidator. Most of the code in this class is boilerplate text that delegates to the parent class. The important part of this class is the implementation of registerTypes(), which registers a singleton instance of each new schema type. For the purposes of this demo, this operation registers the SchemaTypeHex class, which performs schema validation for hexadecimal numbers.

A class that provides a new schema type must implement two operations:

checkRule()
is invoked when the schema type is used in a schema rule.
isA()
is invoked during schema validation of a configuration file.

The SchemaTypeHex class illustrates how to implement the above operations. In addition, the class provides some utility functions that might be useful to application developers: lookupHex(), stringToHex() and isHex().

The FooConfiguration class encapsulates use of Config4* and the new schema type. This class illustrates how to make use of the enhanced schema validator and the utility functions provided by the SchemaTypeHex class.

11.7  Summary

This chapter has discussed the demonstration applications supplied with Config4*. To fully understand the information here, you should examine the source code of the demonstration applications while reading this chapter.

If you want more in-depth advice on how to exploit the full potential of Config4*, then you might wish to read the Config4* Practical Usage Guide.


1
C++ uses "::" as the scoping operator. For a Java application, an entry in the first column of the table might be written as "com.example.mypackage.A.op3".
2
Config4* provides a patternMatch() operation that an application can use for this purpose.

Previous Up Next