This is the Conf4h configuration library. It is a continuation of the Treecster project, but implemented very differently in Scala with incompatible changes in syntax of the configuration file.

 

FAQ

Why?

So, why does the world need yet another configuration framework? Well, it does not. Neither does the software. But humans do! All configuration solutions exist to let users to set parameters based on environment without polluting the source code and these settings are devilishly hard to perform, remember, search for, maintain, scan for differences, adapt for different environments, etc., etc., etc. Conf4h intends to remedy that.

On the simplest side of the problem, the configuration can be represented by a set of name-value pairs. Things get difficult when there are more than a pageful of them. Then all kinds of federated name spaces, multiple repositories, and hierarchical structures start to appear. Since configurations are almost always dependent on the environment, the typical configuration files are supplemented by programs: from C-preprocessor to elaborate m4 macros. On the most complicated side, it sometimes easier to write a whole program that will adapt a configuration to the environment, GNU Autoconf is one example.

What is Conf4h?

Conf4h provides easy hierarchical configuration structures. This is done with advanced hierarchy functions and inheritance of properties, implemented in Scala and available via Java and Scala APIs.

Visit our project site for downloads and information on how to contribute.

What's so great about it?

With Conf4h, you can do away with lengthy configuration files specifying every property for every instance of every application in every environment. Conf4h is designed around the idea of defaults and hierarchy. Unwieldy property files with endless key-value pairs are replaced with single Conf4h file with a syntax similar to HOCON, with defaults specified at each configuration level (i.e. environment, application, instance, etc.) and custom values defined in the sub-levels of the hierarchy. See the example.

Where can I get it?

You can download the latest version at the Sourceforge download page, or you can get the source code by following this link.

Features

The main bragging points are based on combined experience of our shop's development team and reflects our deeply held beliefs of what configuration should and should not be. For details see the examples section.

First Fundamental Belief (FB-1): Type un-safety for simplicity

Configuration properties are always "un-typed" and can be obtained only as Strings. This is the first fundamental belief and the idea is derived from a self-evident truth that if the configuration is expected to be read by humans, it lives in a text file and, therefore, is text. It obviously can be converted to anything a developer would want, but it's not the job of a configuration system. In other words, Conf4h will provide you with strings and you can do whatever else you want with them.

Second Fundamental Belief (FB-2): Properties must be present

If a developer asks for a property that cannot be supplied by the configuration, immediate runtime error will result. This is the second fundamental belief and its origin is in many, many hours spent troubleshooting missing property problems. In so many words, the configuration system is assumed to be the only source of truth and if one relies on configuration system to provide that truth, it better be there.

Third Fundamental Belief (FB-3): Defaults must be explicit

Defaults are prominently featured in Conf4h, but there are rules. We fundamentally believe that defaults must be set explicitly in the configuration file - either on higher levels of the hierarchy or by reference in the macro variables. Macro variable can have their own defaults, but again, they must be present in order to be used.

Fourth Fundamental Belief (FB-4): Environment's influence should be limited

Interaction with environment is only allowed via configuration construction interface. All others are prohibited, discouraged, and frowned upon. Violators will be prosecuted, chastised, and subjected to public humiliation.

Good Ideas (a.k.a. Less Fundamental Beliefs)

Complex data types are supported

Configuration properties are presented as name-value pairs organized in trees. The values can be strings, maps, or lists of strings, maps, lists, etc. Traversing of configuration tree is purposely not supported by the API, but you can get a HOCON string dump for your debugging pleasure.

Inspired inheritance

Since properties are organized as branches of a tree, there is a notion of a "level", i.e., a set of properties accessible via common parent key. The fully qualified name of a key is presented as path with elements separated by "." (dot) for convenience.

Properties from parent levels that are not present in the current level are inherited. This is the fundamental feature of the whole Conf4h concept. It is also where the defaulting functionality comes in: all of the properties that are possibly relevant to a particular instance of a particular application are copied over from the upper levels if not present in the current one.

Inheritance-aware macro interpolation

Conf4h interpolates specially noted macro variables. It allows to use frequently used and environment dependent values by reference. Interpolation is only performed when you explicitly ask for a property that has a macro variable reference in it, for the purpose of propagating changes to the referenced variable.

Configuration file syntax

We share the goals of HOCON's creators to provide a configuration file syntax suitable for human use, hence the name Conf4h, as in "Configuration for(4) humans". Here are major requirements as we see them:

Definitions

Key (or name)
Some string that names a property. All printable characters are allowed except white space, '=' (equal sign), and '"' (double quote). If these are required, enclose the string in double quotes and escape '"' with '\' (back slash).
Value
A string or map or list that is returned for a property found by its key. The rules for strings are the same as for keys.
Property
A name-value pair, where value can be string or map or list.
Map
An unordered collection of properties.
List
An ordered collection of values addressed by an index.
Path
An ordered sequence of keys separated by "." (dot) character.
Variant
A special kind of map representing different paths depending on a specific property.

Name-values

Name and value separated by an optional "=" (equal sign) denote a property:

      a = b
      c = d

Properties must be separated by white space. Values can be strings or nested data structures( lists, maps, and variants).

Strictly speaking, equal signs are not required, but omitting them can make configuration files difficult to read. The above example can be written as

      a b c d

The same is true for other data structures that follow.

Comments and white space

Comments are either in-line delimited by "//" (double slash) or block style enclosed in "/* */":

      a = b // This is an in-line comment
      /* And this is the
      block-style comment */
      c = d 

Comments are allowed anywhere where white space is permitted. White space is allowed everywhere except where prohibited.

Maps

Maps are enclosed in curly braces and must be named unless in a list:

      com = {
        a = b
        c = {
          d = e
        }
      } 

Names of the nested maps create a path. In the above example:

      com.c.d = "e" 

Equal signs between a key and a map can be omitted for simplicity, thus this is the same as the above:

      com {
        a = b
        c {
          d = e
        }
      } 

Lists

Lists are enclosed in square braces, member values must be separated by ',' (commas) or any other white space, equal signs can be omitted:

      com {
        a = [b, c, d]
        e = [f
             g
             h]
        i [
           {
             k = l
           }, { m = n }
           { o = p }
          ]
      } 

Elements of a list can be of different types, whether it is a good idea is up to you. Elements are addressed by their 0-based index. In the above example:

      com.a.1 = "c"
      com.i.2.o = "p" 

Variants

Variant is a special map enclosed in angle brackets, which contains only other maps called variant values:

      r {
        d <
          1 { v=s}
          a { v=v}
          b { w=x}
        > // d
      } // r 

Depending on what the value of "d" is (we call it a selector), this is equivalent to the following pseudo-code:

    if d = "1" -> r.v = "s"
    if d = "a" -> r.v = "v"
    if d = "b" -> r.v - Error!
    otherwise -> Problem! 

A common strategy to avoid surprises with variants is to define defaults:

      r {
        v = ddd
        d <
          1 { v=s}
          a { v=v}
          b { w=x}
        > // d
      } // r 

then

    if d = "1" -> r.v = "s"
    if d = "a" -> r.v = "v"
    anything else ->  r.v = "ddd"

Root level

Each configuration file must contain one or more roots, i.e., maps on the top-most configuration level. For example:

      root {
        a = b
        // ...
      }
      com {
        conf4h {
           // ...
        }
      } 

Macro variables

Macro variables can appear anywhere, where values are allowed, and have the special syntax:

      ${key:default} 

For example,

      root {
        a = say ${b:bar} now
      } 

will evaluate to root.a="say foo now" if b="foo" and root.a="say bar now" if b is undefined. Default value can be omitted, but then the previous example will generate a runtime error if b is not defined.

If the key is complex, i.e., it contains one or more "." separators, the value of the macro will be resolved from the top of the hierarchy. For simple keys resolution starts at the present level.

API

Implementation notes

Current Conf4h is implemented using a custom combination parser based on FastParse library with implementation details mostly hidden.

Both Java and Scala APIs are provides, the differences are as follows:

Java Scala
Main class com.conf4h.Conf4hJ com.conf4h.Conf4h
Maps java.util.Map<String, String> scala.collection.Map[String, String]
Lists java.util.List<String> scala.collection.immutable.List[String]

The rest of the API calls and structures are exactly the same. The examples below are given in Scala though.

Example file

We will use the following example to illustrate the API:

moving {
    env = ${env}
    speed = ${speed}
    
    plan = "Getting away in ${env} environment with ${speed} speed on ${moving.name} using ${moving.propulsion} for power."
    env <
        water {
            propulsion = enthusiasm
            hull = timber
            speed <
                fast {
                    name = Cruiser Port Royal 
                    propulsion = turbine
                    fuel = kerosene
                    hull = [steel]
                } // fast

                moderate {
                    name = Clipper Catty Sark
                    propulsion = wind
                } // moderate

                slow {
                    name = raft
                    fuel = sashimi
                } // slow
            > // speed
        } // water

        air {
            propulsion = jet
            fuel = kerosene

            speed <
                fast {
                    name = fighter jet
                } // fast

                moderate {
                    name = airliner
                    hull = [
                        aluminum
                        composite
                    ]
                } // moderate

                slow {
                    name = balloon
                    propulsion = wind
                    view = [ clouds, houses, {sky=blue, grass=green}, "smoke stacks"]
                    fuel = propane
                } // slow
            > // speed
        } // air
    > // env        
} // moving 

It describes a moving problem in two media (water and air) with three possible speeds (fast, moderate, slow). We'll call the file propulsion.c4h

Initialization

The configuration can be initialized either from a text file or from a string with optional variant map:

      val conf1 = Conf4h.loadString("r {a=b}", Map("c" -> "d"))
      val conf2 = Conf4h.loadFile("myconfig.c4h") 

For example:

      val conf = Conf4h.loadFile("propulsion.c4h", Map("env"->"air", "speed"->"slow")) 

Note, however, the environment settings are ignored. It means, if you want to use command line initialization of the variants like

    java -Denv=air -Dspeed=slow ... scala.tools.nsc.MainGenericRunner ...    

You must explicitly transfer environment variables values to the initial variants map:

      val conf = Conf4h.loadFile("propulsion.c4h", Map("env"->sys.env("env"), "speed"->sys.env("speed"))) 

The interpolation variables use the regular ${var} syntax where the variable may refer to either another property (key) or variant.

Given two different sets of variants, the following results will be obtained:

Property Variants
env=air
speed=moderate
env=water
speed=slow
moving.name airliner raft
moving.plan Getting away in air environment with moderate
speed on airliner using jet for power.
Getting away in water environment with slow
speed on raft using enthusiasm for power.
moving.env air floating in water
moving.speed moderate moving very slow
moving.hull [aluminum, composite] timber

Simple getters

A simple getter is as simple as it gets:

      val x1 = conf.getString("moving.name")    // "balloon"
      val x2 = conf.getString("moving", "name") // "balloon"
      val x3 = conf.getString("moving", "hull") // RuntimeException thrown 

Getters for lists and maps

These are similarly simple:

      val lst = conf.getList("moving", "view") // List("clouds", "houses", "smoke stacks")
      val mp  = conf.getMap("moving") // Map("name"->"balloon", "fuel"->"propane", "propulsion"->"wind")     

Note that "colors" map is not listed in the list and view is not among map's elements - you are getting strings only. If you want to check the "colors", this will get it:

      val mp  = conf.getMap("moving.view.2") // Map("sky"->"blue", "grass"->"green")     

Individual properties and elements of arrays are available via simple getters:

      val three = conf.getString("moving", "view", "3") // "smoke stacks"
      val grass = conf.getString("moving", "view", "2", "grass") // "green"    

Converter to java.util.Properties

Sometimes it's convenient to use Java's own Properties class, especially for third-party components. Conf4h provides a converter method:

      val p: java.util.Properties = conf.toProperties("moving");    

which will return something like

      moving.name=balloon
      moving.propulsion=wind
      moving.fuel=propane
      moving.view.0=clouds
      moving.view.1=houses
      moving.view.2.sky=blue
      moving.view.2.grass=green
      moving.view.3=smoke stacks    

The configuration will be fully processed with variants and interpolations before conversion. Lists are converted to strings by adding an index to the key, as shown above.

There is one caveat, though. java.util.Properties allows the same key to have a value and sub-keys at the same time. For example:

      log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender
      log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout
      log4j.appender.CONSOLE.layout.ConversionPattern = %d [%t] %-5p %c %x - %m%n    

To accommodate such a case, Conf4h uses a special key named '~' (tilde):

      log4j {
          appender {
              CONSOLE {
                  layout {
                      ~=org.apache.log4j.PatternLayout
                      ConversionPattern="%d [%t] %-5p %c %x - %m%n"
                  }
                  ~=org.apache.log4j.ConsoleAppender
              } // CONSOLE
          } // appender
      } // log4j
    

When '~' is encountered during properties conversion, Conf4h creates a key one level up as in the above example, so log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender can also have sub-keys like log4j.appender.CONSOLE.layout, which, in turn, also has a value and a sub-key.

Converter to com.typesafe.config.Config

Converting to Lightbend's configuration is easy:

        val c: com.typesafe.config.Config = conf.toConfig(keys...)
    

Beware that in the converted configuration all the keys are double-quoted to prevent the Lightbend's library from freaking out on HOCON-improper keys. It does not affect its getter methods though.

It is a good idea to isolate your Akka settings from the rest of it, for example

      moving {
          // ...
      }

      my-akka {
          akka {
              loggers = ["akka.event.slf4j.Slf4jLogger"]
              loglevel = "INFO"
              actor {
                  debug {
                      autoreceive = on
                      lifecycle = on
                  }
                  provider = "akka.cluster.ClusterActorRefProvider"
              } // actor

              remote {
                  log-remote-lifecycle-events = off
                  netty.tcp {
                      hostname = "127.0.0.1"
                      port = 0
                  }
              } // remote
          } // akka

          cluster-dispatcher {
              type = "Dispatcher"
              executor = "fork-join-executor"
              fork-join-executor {
                  parallelism-min = 2
                  parallelism-max = 4
              }
          } // cluster-dispatcher
      } // my-akka

      log4j {
          // ...
      }      

Then the value you feed to your actors will be

      val c = com.typesafe.config.ConfigFactory.load(con.toConfig().getConfig("my-akka"))      

This allows to consolidate all of your reference.conf and application.conf files into one main configuration file with application- or environment-specific portions sectioned off.

Configuring Log4j v.1.2

The Log4j v.1.2 configuration can be specified in a separate file or included with any other configuration. The only requirement for it is to follow Log4j's naming hierarchy and to have all required components like appenders, loggers, etc. See Converter to java.util.Properties section for details.

The following environment variables should be used in order to utilize this functionality, both must be present:

       java -Dlog4j.configuration=file:myconfig.c4h -Dlog4j.configuratorClass=com.conf4h.Log4jConfigurator ... 

Note that only file URLs are supported for log4j.configuration now.

Progress

Known problems

Wish list

This is a list of missing features of Conf4h: