Configuration Points
A central concept in Gaderian is configuration extension points.
Once you have a set of services, it's natural to want to configure those
services. In Gaderian, 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 Gaderian 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="gaderian.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 Gaderian does? Or the
line-precise error reporting? Would you bother to create unit tests
for all the failure conditions?
Using Gaderian 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 gaderian.FactoryDefaults and gaderian.ApplicationDefaults
configuration points use the same schema. The Gaderian 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 gaderian.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="gaderian.test.services.SimpleService">
<invoke-factory>
<construcgaderian.testmind.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, Gaderian may be unable to recover and will throw a runtime
exception.
Substitution Symbols
The information provided by Gaderian 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).
Gaderian 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 Gaderian 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. Gaderian itself defines a configuration
configuration point for this purpose: gaderian.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 Gaderian 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:
gaderian.FactoryDefaults
. A second configuration point, for application defaults, overrides
the factory defaults: gaderian.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 Gaderian 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 Gaderian
it is almost entirely absent. Instead, all the XML parsing occurs
inside Gaderian, 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
Gaderian 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.