Configurations

Definition

Most applications and their services need some kind of configuration data that controls the application behaviour. Typical usecases are a database url or a mapping from interfaces to implementation classes for a factory service. At runtime configuration data is mostly represented by "dumb" java classes that doesn't contain any business logic.

Since annocon is a pure java solution there are no limits on how to retrieve configuration data. Nevertheless there are some things annocon can do

  • consistent wiring of services with configuration data
  • easy and consistent access to file based configuration data
  • the concept of contributions as means of realizing multiple data sources
  • merging of configuration data that is contributed from different sources

The following example demonstrates how hard coded configuration data can be defined in a context and is integrated into the wiring of a service:

@Context()
public class ConfigurationContext
{
  @Service(id = "FactoryService")
  public FactoryService getFactoryService()
  {
    return new FactoryService(getFactoryConfig());
  }

  @Configuration(id = "FactoryConfig")
  public Map getFactoryConfig()
  {
    Map config = new HashMap();
    container.put("map", "java.util.HashMap");
    container.put("collection", "java.util.ArrayList");
    container.put("inputStream", "java.io.ByteArrayInputStream");
    return config;
  }
}  

This way of handling configuration already offers some advantages. The configuration data is reusable and annocon guarantees that configurations are singletons. Speaking about pure java, there is no need for parsers and translators that convert string values to other objects. Of course the data can be read from all kinds of external sources. Moreover it is possible to specialize the context class and just override some settings to allow unit tests or to support another environment.

There are more benefits by using contributions or annocons support for file based configuration data.

Contributions

Contributions are a way to modularize your configuration data in order to keep your application loosely coupled. The declaration of a configuration (the schema) is separated from the assembly of the concrete data. For example the FactoryConfig from the last example can be realized by this means:

@Context()
public class ConfigurationContext
{
  ...

  @Configuration(id = "FactoryConfig")
  public Map getFactoryConfig()
  {
    return new HashMap();
  }

  @Contribution(configurationId = "FactoryConfig")
  public void contributeToFactory(Map container)
  {
    container.put("map", "java.util.HashMap");
    container.put("collection", "java.util.ArrayList");
    container.put("inputStream", "java.io.ByteArrayInputStream");
  }

}  

The configuration method is responsible for declaring the type of the configuration and for creating the root object that holds the configuration data. This object is called the configuration container. The container can be of any type. The used type completely depends on the requirements of the developer. Examples:

  • java.util.Properties for string based key/value mappings
  • java.util.List for a list of arbitrary objects
  • dom.Document for a DOM based node hierarchy

Contributions are marked by the @Contribution annotation, The contribution specifies the configuration which it contributes to using the configurationId attribute. A contribution method must define a single parameter. This can be

  • the container that is the root of the object hierarchy, or
  • a configuration parser used for processing file based data

The contribution method in the example above defines a Map as parameter and on execution gets a reference to the HashMap created by getFactoryConfig(). This container parameter must be of the same type that the configuration method returns (or an ancestor). Now the code in the contribution method can just add new elements to the container or override existing ones.

Contributions really make sense, when working with subcontexts. As an example think of a swing application that is build from a couple of plugins (like eclipse). Each plugin wants to add menu items to the central menu bar. The Swing Menu example on the examples page demonstrates the use of this functionality for a plugin mechanism.

Merging contributions

Annocon handles the merging of the configuration data automatically, if multiple contributions to the same configuration exist. The built-in merging supports the standard java collection classes like List, Map, Set. In the case of more complex configuration data, it is possible to specify the merger to use.

File based configuration data

Configuration data is often defined in external text files, typically in a xml format. Examples are the deployment descriptor of a web application, a struts configuration file or the plugin manifest of an eclipse plugin. Thus the application must handle the parsing of the files and optionally map the data to java beans. It is a best practise to free the services from the technical details of parsing.

Annocon doesn't implement another xml to java mapping but delivers built-in support for the Jakarta Digester library (integration of other mapping tools is possible of course). Using annocons contributions and parser functionality it's quite easy to ...

  • read and parse configuration files in a consistent manner
  • mix file based configuration data with runtime data
  • built configuration data from multiple files (schema reuse)

The succeeding code snippet shows how to setup a configuration parser using the @ConfigurationParser annotation. The parser is responsible for reading data from classpath, relative or absolute path or any input stream. One parser instance knows how to parse exactly one file format, for example files for one DTD or xml schema:

               
@Context
public class DigesterContext
{
  @Service(id = "FactoryService")
  public FactoryService getFactoryService()
  {
    return new FactoryService(getConfigParts());
  }

  /**
   * Defines the FactoryConfig. It contains key/class-name combinations.
   * 
   * @return The configuration container, that contains the merged data of all
   *         contributions.
   */
  @Configuration(id = "FactoryConfig")
  public Map getConfigParts()
  {
    return new HashMap();
  }

  /**
   * Sets up a digester instance that can parse xml configuration data for
   * FactoryConfig
   * 
   * @param container the configuration container that receives the parsed data.
   * @return a parser that can parse xml files
   */
  @ConfigurationParser(id = "FactoryConfigParser", configurationId = "FactoryConfig")
  public Parser setupParserForFactoryConfig(Map container)
  {
    Digester digester = new Digester();
    digester.setValidating(false);
    digester.push(container);
    digester.addCallMethod("factory/mapping", "put", 2, new String[] { "java.lang.Object", "java.lang.Object" });
    digester.addCallParam("factory/mapping", 0, "key");
    digester.addCallParam("factory/mapping", 1, "class-name");

    Parser parser = new DigesterParser(digester, container, null);
    return parser;
  }

  /**
   * Contributes hard coded data to the FactoryConfig.
   * 
   * @param container the configuration container.
   */
  @Contribution(configurationId = "FactoryConfig")
  public void contributeToFactory(Map container)
  {
    container.put("map", "java.util.HashMap");
  }

  /**
   * Contributes the content of an xml file to the FactoryConfig.
   * 
   * @param parser the parser that can process file based data
   */
  @Contribution(configurationId = "FactoryConfig")
  public void contributeToFactoryConfig(Parser parser)
  {
    parser.processResource("net/sf/annocon/examples/configuration/digester/factoryConfig.xml");
  }

}
            

The parser is associated with a configuration by its configurationId attribute. Contributions to that configuration, which define a parameter of the type parser, get a reference to this parser handed in. Any number of contributions can use the same parser. The TopdownContext example on the examples page demonstrates the use of this functionality for a distributed configuration.

Manual configuration data and file based configuration data can be mixed.