2009/04/15 - Apache HiveMind has been retired.

For more information, please explore the Attic.

Apache > HiveMind
Apache
 
Font size:      

Configuration Points

A central concept in HiveMind is configuration extension points. Once you have a set of services, it's natural to want to configure those services. In HiveMind, a configuration point contains an unordered collection of elements. Each element is contributed by a module ... any module may make contributions to any configuration point visible to it.

There is no explicit connection between a service and a configuration point, though it is often the case that a service and a configuration point will be similarily named (or even identically named; services and configuration points are in seperate namespaces). Any relationship between a service and an configuration point is explicit only in code ... the service may be configured with the elements of a configuration point and operate on those elements in some way.

Defining a Configuration Point

A module may include <configuration-point> elements to define new configuration points. A configuration point may specify the expected, or allowed, number of contributions:

  • Zero or one
  • Zero or more (the default)
  • At least one
  • Exactly one

At runtime, the number of actual contributions is checked against the constraint and an error is reported if the number doesn't match.

Defining the Contribution Format

A significant portion of an configuration point is the <schema> element ... this is used to define the format of contributions that may be made inside <contribution> elements. Contributions take the form of XML elements and attributes, the <schema> element identifies which elements and which attributes and provides rules that transform the contributions into Java objects.

This is very important: what gets fed into an configuration point (in the form of contributed <contribution>s) is XML. What comes out on the other side is a collection of configured Java objects. Without these XML transformation rules, it would be necessary to write Java code to walk the tree of XML elements and attributes to create the Java objects; instead this is done inside the module deployment descriptor, by specifying a <schema> for the configuration point, and providing rules for processing each contributed element.

If a contribution by a <contribution> element is invalid, then a runtime error is logged and the contribution is ignored. The runtime error will identify the exact location (the file, line number and column number) of the contribution so you can go fix it.

The <schema> element contains <element> elements to describe the XML elements that may be contributed. <element>s contain <attribute>s to define the attributes allowed for those elements. <element>s also contain <conversion> (or <rules>) used to convert the contributed XML into Java objects.

Here's an example from the HiveMind test suite. The Datum class defines two properties: key and value.

<configuration-point id="Simple">
  <schema>
    <element name="datum">
      <attribute name="key" required="true"/>
      <attribute name="value" required="true"/>

      <conversion class="hivemind.test.config.impl.Datum"/>
    </element>
  </schema>
</configuration-point>

<contribution configuration-id="Simple">
  <datum key="key1" value="value1"/>
  <datum key="key2" value="value2"/>
</contribution>

The <conversion> element creates an instance of the Datum class, and initializes its properties from the attributes of the contributed element (the datum and its key and value attributes). For more complex data, the <map> and <rules> elements add power (and complexity).

This extra work in the module descriptor eliminates a large amount of custom Java code that would otherwise be necessary to walk the XML contributions tree and convert elements and attributes into objects and properties. Yes, you could do this in your own code ... but would you really include all the error checking that HiveMind does? Or the line-precise error reporting? Would you bother to create unit tests for all the failure conditions?

Using HiveMind allows you to write the schema and rules and know that the conversion from XML to Java objects is done uniformly, efficiently and robustly.

The end result of this mechanism is very concise, readable contributions (as shown by the <contribution> in the example).

In addition, it is common for multiple configuration points to share the exact same schema. By assigning an id attribute to a <schema> element, you may reference the same schema for multiple configuration points. For example, the hivemind.FactoryDefaults and hivemind.ApplicationDefaults configuration points use the same schema. The HiveMind module deployment descriptor accomplishes this by defining a schema for one configuration point, then referencing it from another:

<schema id="Defaults">
  <element name="default">
    . . .
  </element>
</schema>

<configuration-point id="FactoryDefaults" schema-id="Defaults"/>

Like service points and configuration points, schemas may be referenced within a single module using an unqualified id, or referenced between modules using a fully qualified id (that is, prefixed with the module's id).

Accessing Configuration Points

The central purpose of configurations is to configure services. Thus the most common way of accessing a configuration is having it directly injected into a service implementation by the hivemind.BuilderFactory. A configuration can be injected into a writable property (or also a constructor parameter) of type List or Map.

Assume we have a service SimpleService which we would like to configure with the Simple configuration from the previous example. All we have to do is to define a setter on the service implementation class (for example with signature setData(List)) and declare the service and its implementation in the module descriptor accordingly:

<service-point id="SimpleService" interface="hivemind.test.services.SimpleService">
  <invoke-factory>
    <construct class="hivemind.test.services.impl.SimpleServiceImpl">
      <set-configuration property="data" configuration-id="Simple"/>
    </construct>
  </invoke-factory>
</service-point>

The collection of configuration elements is always injected as an unmodifiable collection. An empty list / map may be injected, but never null.

The order of the elements in the list is not defined. If order is important, you should create a new (modifiable) list from the injected list and sort it.

Note that the elements in the list are no longer the XML elements and attributes that were contributed, the rules provided in the configuration point's <schema> are used to convert the contributed XML into Java objects.

Note
Although it is possible to access configurations via the Registry (via its getConfiguration(String) method), it is often not a good idea. It is unlikely that you want the information contained in a configuration as an unordered list. A best practice is to always access the configuration through a service, which can organize and validate the data in the configuration.

Accessing Configurations as a Map

As mentioned it is also possible to have the configuration contributions injected as a Map. This requires the schema to define the attribute of the top-level elements which should be used as the key for the elements in the map. This is specified using <element>'s key-attribute attribute. The identified key attribute is implicitly marked as required and unique.

So the previous configuration point Simple can also be defined as follows:

<configuration-point id="Simple">
  <schema>
    <element name="datum" key-attribute="key">
      <attribute name="key"/>
      <attribute name="value" required="true"/>

      <rules>
        <push-attribute attribute="value"/>
        <invoke-parent method="addElement"/>
      </rules>
    </element>
  </schema>
</configuration-point>

The resulting configuration point is now accessible as a Map, where the translated value of the key attribute is the key and the translated value of the value attribute is the value of the Map.Entry elements.

Note
It is also possible to access the elements of this configuration point as a List, but the elements therein are now the objects (in this case Strings) created by the <push-attribute> rule.

Lazy Loading

At application startup, all the module deployment descriptors are located and parsed and in-memory objects created. Validations (such as having the correct number of contributions) occur at this stage.

The list of elements for a configuration point is not created until a service implementation, into which the configuration is being injected, is constructed or until the first call to Registry.getConfiguration() for that configuration point.

In fact, it is not created even then. When the element list for an configuration point is first accessed, what's returned is not really the list of elements; it's a proxy, a stand-in for the real data. The actual elements are not converted until they are actually needed, in much the same way that the creation of services is deferred.

In general, you will never know (or need to know) this; when you access the size() of the list or get() any of its elements, the conversion of contributions into Java objects will be triggered, and those Java objects will be returned in the list.

If there are minor errors in the contribution, then you may see errors logged; if the <contribution> contributions are singificantly malformed, HiveMind may be unable to recover and will throw a runtime exception.

Substitution Symbols

The information provided by HiveMind module descriptors is entirely static, but in some cases, some aspects of the configuration should be dynamic. For example, a database URL or an e-mail address may not be known until runtime (a sophisticated application may have an installer which collects this information).

HiveMind supports this notion through substitution symbols. These are references to values that are supplied at runtime. Substitution symbols can appear inside literal values ... both as XML attributes, and as character data inside XML elements.

Example:

<contribution configuration-id="com.myco.MyConfig">
  <value> dir/foo.txt </value>
  <value> ${config.dir}/${config.file} </value>
</contribution>

This example contributes two elements to the com.myco.MyConfig configuration point. The first contribution is simply the text dir/foo.txt. In the second contribution, the content contains substitution symbols (which use a syntax derived from the Ant build tool). Symbol substitution occurs before <schema> rules are executed, so the config.dir and config.file symbols will be converted to strings first, then whatever rules are in place to convert the value element into a Java object will be executed.

Note
If you contribute text that includes symbols that you do not want to be expanded then you must add an extra dollar sign to the false symbol. This is to support legacy data that was already using the HiveMind symbol notation for its own, internal purposes. For example, foo $${bar} baz will be expanded into the text foo ${bar} baz.

Symbol Sources

This begs the question: where do symbol values come from? The answser is application dependent. HiveMind itself defines a configuration configuration point for this purpose: hivemind.SymbolSources. Contributions to this configuration point define new objects that can provide values for symbols, and identify the order in which these objects should be consulted.

If at runtime none of the configured SymbolSources provides a value for a given symbol then HiveMind will leave the reference to that symbol as is, including the surrounding ${ and }. Additionally an error will be logged.

Frequently Asked Questions

  • Are the any default implementations of SymbolSource?

    There is now an configuration point for setting factory defaults: hivemind.FactoryDefaults . A second configuration point, for application defaults, overrides the factory defaults: hivemind.ApplicationDefaults.

    SystemPropertiesSymbolSource is a one-line implementation that allows access to system properties as substitution symbols. Note that this configuration is not loaded by default.

    Additional implementations may follow in the future.

  • What's all this about schemas and rules?

    A central goal of HiveMind is to reduce code clutter. If configuration point contributions are just strings (in a .properties file) or just XML, that puts a lot of burden on the developer whose code reads the configuration to then massage it into useful objects. That kind of ad-hoc code is notoriously buggy; in HiveMind it is almost entirely absent. Instead, all the XML parsing occurs inside HiveMind, which uses the schema and rules to validate and convert the XML contributions into Java objects.

    You can omit the schema, in which case the elements are left as XML (instances of Element) and your code is responsible for walking the elements and attributes ... but why bother? Far easier to let HiveMind do the conversions and validations.

  • How do I know if the element list is a proxy or not?

    Basically, you can't, short of performing an instanceof check. There isn't any need to tell the difference between the deferred proxy to the element list and the actual element list; they are both immutable and both behave identically.