Previous Up Next

Chapter 6  The config2cpp and config2j Utilities

6.1  Introduction

The config2cpp and config2j utilities read a configuration file and generate a C++ or Java file that contains a class wrapper around a snapshot of the file’s contents. These utilities make it easy to generate a configuration string that can be embedded in an application. This can be useful in an embedded system that does not contain a file system. It is also useful if you want an application to have “fallback” configuration (discussed in Section 3.6.3).

6.2  Basic Operation

The config2cpp utility is a compiled application, while config2j is a Windows batch file or UNIX shell script that executes the main() operation of the org.config4j.Config2J class.

Figure 6.1 shows a file, Fallback.cfg, that contains the configuration information we want to embed as the fallback configuration for an application. We can do this for a C++ or Java application by running one of the following commands:

config2cpp -cfg Fallback.cfg -class FallbackConfig
config2j   -cfg Fallback.cfg -class FallbackConfig
Figure 6.1: The file Fallback.cfg
timeout = "infinite";
log {
  dir = ".";  # current working directory
  level = "1";
}
TCP {
  buffer_size = "8 KB";
  threading_policy = "thread_pool";
  max_threads = "10";
}
SSL {
  buffer_size = "8 KB";
  threading_policy = "thread_pool";
  max_threads = "10";
}

Figure 6.2 shows the Java class generated from the file shown in Figure 6.1; a generated C++ class would be structurally similar. The constructor initialises two instances variables, schema and str, that can be accessed by calling the public operations getSchema() and getString().

Figure 6.2: Java class generated by config2j
class FallbackConfig
{
  public FallbackConfig()
  {
    schema = new String[12];
    schema[0] = "SSL = scope";
    schema[1] = "SSL.buffer_size = memorySizeBytes";
    schema[2] = "SSL.max_threads = int";
    schema[3] = "SSL.threading_policy = string";
    schema[4] = "TCP = scope";
    schema[5] = "TCP.buffer_size = memorySizeBytes";
    schema[6] = "TCP.max_threads = int";
    schema[7] = "TCP.threading_policy = string";
    schema[8] = "log = scope";
    schema[9] = "log.dir = string";
    schema[10] = "log.level = int";
    schema[11] = "timeout = durationSeconds";
    str = new StringBuffer();
    str.append("timeout = \"infinite\";" + CR);
    str.append("log {" + CR);
    str.append("  dir = \".\";  # current working directory" + CR);
    str.append("  level = \"1\";" + CR);
    str.append("}" + CR);
    str.append("TCP {" + CR);
    str.append("  buffer_size = \"8 KB\";" + CR);
    str.append("  threading_policy = \"thread_pool\";" + CR);
    str.append("  max_threads = \"10\";" + CR);
    str.append("}" + CR);
    str.append("SSL {" + CR);
    str.append("  buffer_size = \"8 KB\";" + CR);
    str.append("  threading_policy = \"thread_pool\";" + CR);
    str.append("  max_threads = \"10\";" + CR);
    str.append("}" + CR);
    str.append("");
  }
  public String[] getSchema() { return schema; }
  public String   getString() { return str.toString(); }
  private String[] schema;
  private StringBuffer str;
  private static final String CR= System.getProperty("line.separator");
}

For the moment, ignore the initialisation of schema (I will discuss that in Section 6.4), and instead look at the initialisation of str. That variable is initialised to hold a copy of the contents of the file given as a command-line argument to config2j or config2cpp.

By default, your application would have to create an instance of the generated class before invoking getSchema() or getString(). That explicit creation step can be avoided by using the -singleton command-line option, which causes the generated class to provide an automatically-created singleton object; getString() and getSchema() become static operations that delegate to the singleton object.

config2cpp -cfg Fallback.cfg -class FallbackConfig -singleton
config2j   -cfg Fallback.cfg -class FallbackConfig -singleton

By default, the generated class is not in any Java package or C++ namespace. The -package <name> or -namespace <name> command-line option can be used to generate the class in a specified package or namespace. For example:1

config2cpp -cfg Fallback.cfg -class FallbackConfig -singleton \
         -namespace x::y::z
config2j   -cfg Fallback.cfg -class FallbackConfig -singleton \
         -package com.example.foo

6.3  Using the Generated Class

Let’s assume we have converted the file Fallback.cfg into a Java class called FallbackConfig with the following command:

config2j -cfg Fallback.cfg -class FallbackConfig -singleton \
         -package com.example.foo

Figure 6.3 shows how the FallbackConfig class can be used to provide fallback configuration for an application.

Figure 6.3: Example of Using Config4J
Configuration cfg = Configuration.create();
String cfgFile = ...
try {
  if (cfgFile != null) { cfg.parse(cfgFile); }
  cfg.setFallbackConfiguration(Configuration.INPUT_STRING,
                               FallbackConfig.getString());
} catch(ConfigurationException ex) {
  System.out.println(ex.getMessage());
}

The code in Figure 6.3 is straightforward. It parses a configuration file if one was specified by, say, a command-line option or an environment variable. Afterwards, it calls setFallbackConfiguration() to set fallback configuration based on the string in the FallbackConfig class. Because we had run config2j with the -singleton option, the code can call FallbackConfig.getString(). If we had not used the -singleton option, then the code would have had to explicitly create an instance of the FallbackConfig class, for example:

cfg.setFallbackConfiguration(Configuration.INPUT_STRING,
                             new FallbackConfig().getString());

6.4  Tweaking the Generated Schema

If you look again at Figure 6.2, you will see that the class generated by config2cpp or config2j provides not just getString() but also getSchema(). The code at the start of the constructor initializes the schema returned by getSchema().

The config2cpp and config2j utilities employ the following heuristics to generate a schema from an input configuration file.

Those heuristics work well most of the time. However, they can make mistakes. As an example, consider the threading_policy variable in the TCP and SSL scopes of Figure 6.1. That has the value "thread_pool", which might be an enum value rather than merely a string. As another example, perhaps the log.level variable should be represented in the schema as being an integer with a limited range of values, such as int[0,3].

You can provide config2cpp and config2j with instructions on how to generate the schema. To do this, you need to write another configuration file as shown in Figure 6.4. That file contains three variables that tweak the schema-generation heuristics of config2cpp and config2j.

Figure 6.4: The file SchemaFineTuning.cfg
user_types = [
  "@typedef threadingPolicy = enum[thread_pool, thread_per_socket]"
];
wildcarded_names_and_types = [
  # optional/required   wildcarded name          Schema type
  #----------------------------------------------------------------
   "@optional",         "log.level",             "int[0,3]",
   "@optional",         "*.threading_policy",    "threadingPolicy",
];
ignore_rules = [
];

The user_types variable defines a list of types that are to be included in the generated schema. The wildcarded_names_and_types variable is a three-column table. Within each row of this table, the first column contains a keyword (either @optional or @required), the second column contains the name of a configuration variables, and the third column specifies its schema type. The meaning of the @optional and @required keywords will be discussed in Section 9.2.2; @optional is the appropriate keyword to use in most circumstances. The name in the second column can contain "*", which acts as a wildcard that matches zero or more characters. Thus, "*.threading_policy" matches both "SSL.threading_policy" and "TCP.threading_policy". The ignore_rules variable is used to specify ignore rules, which will be discussed in Section 9.2.6. For the example being discussed, we do not need any ignore rules, so we set this variable to be an empty list.

Having written that file, you use the -schemaOverrideCfg command-line option to pass the file to config2j or config2cpp:

config2j -cfg Fallback.cfg -class FallbackConfig -singleton \
         -package com.example.foo \
         -schemaOverrideCfg SchemaFineTuning.cfg

You can see the results in Figure 6.5.

Figure 6.5: The effects of tweaking the schema
class FallbackConfig
{
  public FallbackConfig()
  {
    schema = new String[13];
    schema[0] = "@typedef threadingPolicy = enum[thread_pool, thread_per_socket]";
    schema[1] = "SSL = scope";
    schema[2] = "SSL.buffer_size = memorySizeBytes";
    schema[3] = "SSL.max_threads = int";
    schema[4] = "@optional SSL.threading_policy = threadingPolicy";
    schema[5] = "TCP = scope";
    schema[6] = "TCP.buffer_size = memorySizeBytes";
    schema[7] = "TCP.max_threads = int";
    schema[8] = "@optional TCP.threading_policy = threadingPolicy";
    schema[9] = "log = scope";
    schema[10] = "log.dir = string";
    schema[11] = "@optional log.level = int[0,3]";
    schema[12] = "timeout = durationSeconds";
    ... // code to initialize str omitted for brevity
  }
  ...
}

Having generated an accurate schema, we can now use it to perform schema validation. Figure 6.6 provides an example of this.

Figure 6.6: Using the generated schema
Configuration cfg = Configuration.create();
SchemaValidator sv = new SchemaValidator();
String scope = ...
String cfgFile = ...
try {
  if (cfgFile != null) { cfg.parse(cfgFile); }
  cfg.setFallbackConfiguration(Configuration.INPUT_STRING,
                               FallbackConfig.getString());
  sv.parseSchema(FallbackConfig.getSchema());
  sv.validate(cfg, scope, "");
} catch(ConfigurationException ex) {
  System.out.println(ex.getMessage());
}

6.5  Summary

The config2cpp and config2j utilities read a configuration file and generate a C++ or Java file that contains a class wrapper around a snapshot of the file’s contents. These utilities make it easy to generate a configuration string that can be embedded in an application. This can be useful in an embedded system that does not contain a file system. It is also useful if you want an application to have fallback configuration.

The class generated by the config2cpp and config2j utilities can provide not just embedded configuration, but also a schema. The built-in heuristics for generating the schema definition work well most of the time. You can use the -schemaOverrideCfg command-line option to specify some tweaks for the generated schema.


1
The backslash indicates line continuation.

Previous Up Next