Regis

Overview

Regis stands for "registry". It implements a hierarchical configuration registry that is meant to externalize application configuration. Three main objectives triggered the launch of this project:

  • To provide a powerful, well-designed configuration framework.
  • To allow for configuration persistency (currently using Hibernate and Prevayler).
  • To allow for distributed configuration (currently through Sapia's Ubik distributed computing framework).

Features

  • Multiple implementations: in-memory, persistent (based on Hibernate), distributed, cached.
  • Variable interpolation.
  • Configuration reuse (through node inheritance, inclusion, linking).
  • XML-based configuration loading.
  • Conditional directives.
  • Simple API.

A presentation of Regis' different usage patterns is available.

Design

Regis is designed as follows:

  • A Registry is a hierarchy of Nodes.
  • A Registry has a single root Node.
  • A Registry is typically instantiated through a RegistryFactory.
  • A Node has a name (identifying it uniquely under its parent Node), and an absolute Path, representing the Node's fully qualified name, starting from the root Node.
  • The RWNode interface is implemented by Nodes that provide "write" operations.

Learning by Example

The examples illustrate how to use Regis, from an application programming perspective. For more details, see the javadoc.

The general usage pattern involves:

  • Acquiring a registry (usually through a RegistryProvider).
  • Opening a RegistrySession.
  • Performing the desired registry operations.
  • Closing the session.

The Regis API

Acquiring the Root Node

import org.sapia.regis.*

...
Registry reg = // here acquire registry 
RegisSession session = reg.open();
try{
  Node root = reg.getRoot();
  ...
}finally{
  session.close();
}
...

Looking up a Node

// by name
Node child = root.getChild("someName");

// by path
child = root.getChild(
  Path.parse("level_one/level_two/child"));

// by query
child = root.getChild(
  Query.create("level_one/level_two/child")
  .addCrit("prop1", "value1")
  .addCrit("prop2", "value2"));

Creating a Node

// if the implementation you use is an instance 
// of RWNode, you cast it and have access to
// write-operations on the node. Futhermore, your session
// must also support write operations (if that is the
// case you can cast it to a RWSession)

RWSession rwSess = (RWSession)session;
RWNode rwRoot = (RWNode)root;
rwSess.begin();
try{
  Node child = rwRoot.createChild("someChild");
  rwSess.commit();
}
catch(RuntimeException e){
  rwSess.rollback();
}

Getting Properties

Property timeout = child.getProperty("timeoutMillis");
if(timeout.isNull()){
  throw new IllegalStateException("Timeout not specified");
}
else{
  long to = timeout.asLong();
  ...
}

Map properties = child.getProperties();
String timeoutStr = (String)properties.get("timeoutMillis");
...

Child Nodes

Iterator children = parent.getChildren();
while(children.hasNext()){
  Node child = (Node)children.next();
  System.out.println("Got child: " + child.getName());
}

Interpolation

Map vars = new HashMap();
vars.put("cache.dir", System.getProperty("user.dir"));

// let's say stored property value is ${cache.dir}/sessionCache
Property prop = node.renderProperty("cacheDir", vars);

Various Methods

System.out.println("Name: " + child.getName());
System.out.println("Path: " + child.getAbsolutePath());
System.out.println("Is Root: " + child.isRoot());

Iterator names = child.getPropertyNames().iterator();
while(names.hasNext()){
  String name = (String)names.next();
  System.out.println("Property " + name + 
    ": " + child.getProperty(name).asString());
}

Registry Factories

In order to be able to access configuration properties, one has to acquire a Registry instance. That is done using a RegistryContext.

The RegistryContext class plays a role analogous to the InitialContext in JNDI: an InitialContext is given a set of properties, among which is the name of the underlying JNDI context factory corresponding to the desired JNDI implementation.

Hence, a registry context is given the name of the RegistryFactory class to use in order to instantiate a registry (that class name must be mapped to the org.sapia.regis.factory property). In addition, the context is given all the parameters necessary to instantiate the underlying registry; the context passes these parameters (in the form of a java.util.Properties object) to the registry factory that it instantiates. Thus, the context's only use is to instantiate the factory and delegate to that factory instantiation of the registry. The following snippet illustrates what we mean:

Properties props = new Properties();
props.setProperty(RegistryContext.FACTORY_CLASS, 
  "<someClassName>");
// here set other properties has needed...
RegistryContext context = new RegistryContext();
Registry regis = context.connect(props);
...

Now, the above is neat if you know that the underlying registry factory will always be the same. But oftentimes, that won't be the case. When dealing with multiple environments (developer workstation, dev, QA, prod, etc), the way configuration is accessed (or in other words, the type of registry applications use) will mostly be different. This is especially true on developer workstations, where the registry used locally in development will most preferrably be the LocalRegistry (as compared to other environments, where the RemoteRegistry will be favored). The javadoc of the RegistryContext class explains registry boostrapping in details, but let's say for starters that you can specify (as a property) a list of property resources that will be sequentially processed in order to acquire the properties necessary to instantiate a registry:

Properties props = new Properties();
props.setProperty(RegistryContext.BOOTSRAP, 
  "${user.home}/regis/registry.properties, com/acme/conf/regis.properties");
// here set other properties has needed...
RegistryContext context = new RegistryContext();
Registry regis = context.connect(props);
...

In the above, the registry context uses the value of the bootstrap property (interpreted as a comma-delimited list of resources) in order to load the properties used to initialize itself. The first resource that can be found is used, and the search process stops at that point. In the above example, provided that no registry.properties file exists under ${user.home}/regis/registry.properties in production, the properties given last are used (which in this case could point to a distributed registry, shared by distributed applications). Thus, a convention in this case would be to have developers put a registry.properties file under their home directory, which would then bypass the production settings.

Local Registry Factory

The LocalRegistryFactory instantiates an in-memory registry intended for testing or non-distributed, embedded use. The registry supports write-operations (it creates RWNodes), but is not transactional - even though it creates instances of the RWSession interface. The following code shows how to instantiate an in-memory registry:

Properties props = new Properties();
props.setProperty(RegistryContext.FACTORY_CLASS, 
  "org.sapia.regis.local.LocalRegistryFactory");
RegistryContext context = new RegistryContext();
Registry regis = context.connect(props);
RegisSession sess = regis.open();
try{
  Node root = regis.getRoot();
  ...
}finally{
  sess.close();
}

Hibernate Registry Factory

The HibernateRegistryFactory instantiates a registry based on Hibernate. The registry supports write-operations (it creates RWNodes and RWSessionss). The following code shows how to instantiate a Hibernate registry:

Properties props = new Properties();
props.setProperty(RegistryContext.FACTORY_CLASS, 
  "org.sapia.regis.hibernate.HibernateRegistryFactory");
// load the  properties...
InputStream is = 
  new FileInputStream("someDir/hibernate.properties");
props.load(is);

RegistryContext context = new RegistryContext();
Registry regis = context.connect(props);
...

Typically, client applications will make use of read-only operations. But the Hibernate registry supports write-operations. In this case, a RWSession is opened, the desired operations are performed, the changes are committed/rolled back, and eventually the session is closed:

RWSession session = (RWSession)regis.open();

try{
  RWNode rwNode = (RWNode)regis.getRoot();
  Node child = rwNode.createChild("someChild");
  session.commit();
}catch(RuntimeException e){
  session.rollback()
  throw e;
}finally{
  session.close();
}
...

Note that the Hibernate registry factory expects the following properties to be set, providing default values for the others:

  • hibernate.dialect
  • hibernate.connection.driver_class
  • hibernate.connection.url
  • hibernate.connection.username
  • hibernate.connection.password

In addition, the implementation uses c3p0 as a database connection pool implementation. More information of Hibernate's configuration properties (as well as c3p0 properties) can be found on the Hibernate site.

The Hibernate registry makes use of Hibernate's second-level cache, based on EHCache.

Prevayler Registry Factory

As you might guess, using a Registry instantiation by a PrevaylerRegistryFactory allows you configuration to be kept across process restarts. Using the class is straightforward, since it follows the same pattern as the previous factories (having a look at the javadoc should be good enough to be able to start using the Prevayler registry).

The intent behind the Prevayler registry is to in fact use it in conjunction with a registry server. This allows starting persistent registry nodes over the network that synchronize each other's state, and also to remotely upload configuration into these nodes. On the client-side, using a caching registry spares network roundtrips.

Caching Registry Factory

A CacheRegistryFactory is meant to wrap another RegistryFactory instance, and provides caching support on top of it.

The cache registry thus wraps nodes returned by the underlying registry with special nodes that are in fact proxies on top of the original ones. A cache node caches all properties of the node that it wraps, refreshing its state at a predefined interval.

A cache registry can be used to spare hits to the original registry. For example, if wrapping a Hibernate registry, the cache node spares database hits and roundtrips.

The following illustrates how to instantiate a cache registry - which in this cas will wrap a Hibernate registry:

Properties props = new Properties();

props.setProperty(RegistryContext.FACTORY_CLASS, 
  "org.sapia.regis.cache.CacheRegistryFactory");

// setting the refresh interval in seconds
props.setProperty(CacheRegistryFactory.REFRESH_INTERVAL, 
  "org.sapia.regis.cache.interval");

// this following property is used to indicate to the 
// CacheRegistryFactory which factory it will in turn wrap
props.setProperty(CacheRegistryFactory.FACTORY_CLASS, 
  "org.sapia.regis.hibernate.HibernateRegistryFactory");

// here set the properties expected by the 
// HibernateRegistryFactory
InputStream is =
  new FileInputStream("someDir/hibernate.properties");
props.load(is);

RegistryContext context = new RegistryContext();
Registry regis = context.connect(props);

// using the registry normally
RegisSession session = regis.open();
try{
  ...
}finally{
  session.close();
}

As can be seen. the class of the registry factory to wrap is passed as a value for the following property: .....

The nodes returned by a CacheRegistry do not support write-operations.

Remote Registry Factory

The remote registry relies on Sapia's Ubik distributed computing framework to offer a robust distributed configuration registry. It benefits from Ubik's fail-over and load-balancing features. The remote registry is composed of two parts: the client-side and the server-side.

Remoting a registry involves two steps:

  • Making the registry available on the network (server-side)
  • Connecting to the remote registry (client-side)
Making a Registry available on the network

To make a registry available on the network through Ubik, two choice are availale:

  • Export the registry as a server on a given port.
  • Bind a the registry to Ubik's distributed JNDI.

To benefit from fail-over and load-balancing, the second option is the way to go. But in any case, here are examples for both:

// instantiate an exporter (passing in a Registry instance)
RegistryExporter exporter = new RegistryExporter(registry);

System.setProperty("ubik.rmi.address-pattern", "localhost");

// binding to a given port
exporter.bind(40000);

// binding to a Ubik JNDI server

Properties props = new Properties();
props.setProperty(InitialContext.PROVIDER_URL, 
             "ubik://localhost:1099/");
props.setProperty("ubik.jndi.domain", "someDomain"); 
exporter.bind("regis", props);

Setting the regexp corresponding to the IP address pattern (through the ubik.rmi.address-pattern system property) forces Ubik to select a network interface that will match the regexp, and on which it will have the server listen. See Ubik's doc for more info.

In the first case, the registry is bound to the network under a specific port; in the second case, the regitry is bound to an anonymous port and its stub published to Ubik's JNDI. In that latter case, publishing the registry to the JNDI automatically makes it benefit from Ubik's fail-over and load-balancing features. This allows publishing multiple registries on the network, with each acting as a backup for the others - provided they hold identical configurations. The exporter makes use of Ubik's client-side JNDI implementation. Please see the Ubik doc for more info the above-specified JNDI-related properties.

For more details, have a look at the RegistryExporter class.
Looking Up a Remote Registry

From a client application's point of view, looking up a remote registry involves the same steps as using any other registry. In this case, though, a RemoteRegistryFactory must be used, configured with the appropriate properties. The following snippet shows how to connect to a remote registry bound to a specific port, and one bound to the JNDI, under a given name:

// looking up a Registry bound to a specific host/port
Properties props = new Properties();
props.setProperty(RegistryContext.FACTORY_CLASS, 
                  RemoteRegistryFactory.class.getName());
props.setProperty(RemoteRegistryFactory.ADDRESS, "localhost");
props.setProperty(RemoteRegistryFactory.PORT, "40001");    

// looking up a Registry bound to Ubik's JNDI
Properties props = new Properties();
props.setProperty(RegistryContext.FACTORY_CLASS, 
                  RemoteRegistryFactory.class.getName());
props.setProperty(RemoteRegistryFactory.JNDI_NAME, "regis");  
props.setProperty(InitialContext.PROVIDER_URL, 
             "ubik://localhost:1099/");
props.setProperty("ubik.jndi.domain", "someDomain"); 
RegistryContext ctx = new RegistryContext(props);
Registry reg = ctx.connect();
...

For more details, have a look at the RemoteRegistryFactory class.

Property Structure

Regis provides different mechanisms to allow for reusing properties.

Inheritance

A registry is a hierarchy of nodes, each node having a parent (except the root node) and, potentially, child nodes. Property resolution in Regis can rely on node inheritance in order to retrieve properties that are not present at the level of a given node, but can be found at the one of an ancestor node.

By default, node inheritance is not enabled. This means that by default, the isInheritsParent() method of the Node interface returns false. In order to enable node inheritance, the "inheritsParent" flag has to be set to true - this can be done when configuring nodes in a registry, in will be shown in the Configuration section further below.

Property resolution, in the context of node inheritance, works as follows: the required property is looked up at the current node; if it is found, it is returned; if not and node inheritance is enabled, the ancestor is searched, and so on until a matching property is found - or until all relevant nodes have been searched and no property could be found.

Links

Node inheritance might not prove sufficient (or desired) in complex configuration cases: sometimes it can have undesired side effects, or sometimes it might be relevant to have given properties be "present" at the level of a given node without that node being a descendant of the node from which the properties originate.

In that case, node linking can be used. In fact, node linking was introduced to provide the behavior of node inheritance, without the ancestor/descendant relationship.

A link is created by associating an existing node to another. In this case, the relationship is not a composition, but very much an association: the associated node is not "bound" to the lifecycle of the one to which it is linked, neither is it consider a child of that node.

Prepended Links

There are two types of links: prepended links and appended links. In the context of property resolution, prepended links consist of nodes that are searched prior to the current node, when looking up a given property. More precisely, prepended links act has if they were overriding the current node.

When a node has a prepended link to another node, or to multiple other nodes, property resolution works as follows: all such links (nodes) are searched for the required property, in the order in which they were added. If the property could not be found, then the matching property at the current node is returned - if it exists.

Appended Links

Appended links have a more intuitive behavior: such links do not override the node to which they are added, but are rather overriden by it. Therefore, when resolving properties, the following happens: the desired property is searched at the current node. If it is found, it is returned; otherwise, all appended links (nodes) are searched, in the order in which they were added, until the desired property could be found, or until none could be found.

Includes

Includes correspond to nodes that are "physically" added to other nodes, yet not as children. Included nodes only appear to be present as child nodes, because in fact the node to which they are added is not set as their parent. This means that included nodes have another parent, but seem to be physically contained by the node in which they are included.

More precisely, included nodes have the following characteristices

  • A node will be accessible by its name under the node to which it is included. This means that if node B is included into node A, and that the name of node B is "nodeB", the following will return node B: A.getChild("nodeB"). The same explanation applies to path access. the following will return node B (provided node A' name is "nodeA", and lookup is done from the root node of the registry): root.getChild(Path.parse("nodeA/nodeB")).
  • A node that is included under another node will be returned in that node's child collection, when the getChildren() method is called. Do not be mistaken though: included nodes do not have their including node as their parent. In fact, a node can be included in multiple other nodes...

Property resolution, in the context of included nodes, does not work as in other types of relationships. In fact, property resolution at a given node does not "cascade" to included nodes. Included nodes have simply been introduced in order to allow reusing nodes that have been previously defined in the registry: imagine that you defined different sets of user accounts (each account corresponding to a node), for the different department in your organization: helpdesk, managers, sales, etc. You would have the "helpdesk", "managers", "sales" nodes, and under each one, the accounts (nodes) that correspond to each of these groups. Now, imagine that you have defined application nodes: "accounting", "callcenter", "crm", and you want to add specific accounts or groups of accounts under each of these nodes, to reflect who is given access to what. In this case, you do not want to redefine, under the different application nodes, the accounts or groups of accounts that you've already set up. You rather want to reuse them.

That's where includes come in: you include, to each application node, the nodes corresponding to the groups/users that have access to the that application. And then, using Regis' node lookup API (by name, by path by query, getting the children), you can build your security layer, based on the account/application structure you have defined in the registry.

Mixing Inheritance and Links

Mutiple node reuse patterns can be implemented at once: inheritance, prepended links, appended links. Property resolution globally works as follows:

  • Prepended links of the current node are searched. If a property could be found, it is returned.
  • If no property could be found, the current node is is searched (a match is attempted on its own properties). If a property could be found, it is returned.
  • Otherwise, if the current node has its inheritance enabled, the parent is searched (respecting this algorithm). If a property could be found, it is returned.
  • If no property could be found, the appended links are searched. If a property could be found, it is returned.
  • In the end, if no property could be found, a null property is returned.

Variable Interpolation

The node interface supports variable interpolation. This allows using property values as variables in other property values.

How it Works

Imagine for example that you set up a registry with JDBC connection parameters to different databases; each database connection configuration corresponds to a node, as illustrated conceptually below:

databases
  db1
    username: fu
    password: bar
    url: jdbc:hsqldb:mem:database1
    driver: org.hsqldb.jdbcDriver
  db2
    username: sna
    password: fu
    url: jdbc:hsqldb:mem:database2
    driver: org.hsqldb.jdbcDriver
 ...

Now, we observe two things here: both connection will share the same driver. In addition, both connections will share the same database server, but not the same database in that server. Taking that into consideration, you opt to redesign your configuration as follows (remember that the example below does not correspond to the Regis configuration format, but is strictly meant to better illustrate the variable interpolation feature):

databases
  common
    driver:org.hsqldb.jdbcDriver
    baseUrl:jdbc:hsqldb:mem:
  connections 
    db1
      username: foo
      password: bar
      url: ${baseUrl}database1
      --
      append link: common
    db2
      username: sna
      password: fu
      url: ${baseUrl}database2
      --
      append link: common
    ...

There are three things to notice:

  1. The configuration has been restructured in order to profit from node/property reuse and variable interpolation: since the driver is the same, and the database server is the same, a "common" node has been defined, which holds the properties (or parts of the propertries) that are common to all database nodes. Then the common node is appended as a link to each database node.
  2. The driver property has been removed from each database node, since from now on each will "inherit" that property from the common node, by virtue of linking.
  3. The most important part, that we want to illustrate in this example: the url property value has been rewritten to embed a variable (to be understood as such by Regis, it must respect the ${var_name} notation). That variable will be resolved at runtime; in such cases, Regis expects variables to correspond to the name of existing properties. When performing variable interpolation, Regis employs the same algorithm as the one defined in the previous section, with regards to node inheritance and linking. If no value could be found for the given variable, then it is simply not interpolated; no error is generated, and in the above case, the value of the url property would be "${baseUrl}/database2".

When it is Applied

Interpolation is not performed at registry configuration time. It rather occurs on a per-node basis, at runtime. More precisely, it occurs when:

  • One of the getProperties() method is called on a Node instance.
  • One of the renderProperty() method is called on a Node instance.

XML Configuration

Regis supports a convenient configuraton format that can be used to create/update/delete nodes in a registry. XML fits very well with Regis' hierarchical design. The following (used as part of Regis' test suite) consists of a configuration that creates a node hierarchy in a registry (note that variant of the following format is possible - see further below for more info):

<registry>
  <node name="databases">
    <node name="000">
      <property name="username" value="jsmith" />
      <property name="password" value="foo123" />      
      <property name="url"      value="jdbc://saturn:5432/" />            
    </node>
    <node name="001" id="db1">
      <property name="username" value="stiger" />
      <property name="password" value="bar123" />      
      <property name="url"      value="jdbc://pluto:5432/" />                
    </node>    
    <node name="002">
      <link ref="db1" />
      <property name="url"      value="jdbc://mercury:5432/" />                      
    </node>        
  </node>
  <node name="users">
    <node name="backoffice" id="bo">
      <node name="account1">
        <property name="username"  value="cbrown" />
        <property name="firstName" value="Charlie" />        
        <property name="lastName"  value="Brown" />                
        <property name="password"  value="lupus9890!" />        
      </node>
      <node name="account2">
        <property name="username"  value="dmenace" />
        <property name="firstName" value="Dennis" />        
        <property name="lastName"  value="Menace" />                
        <property name="password"  value="canis$2677" />        
      </node>      
    </node>
    <node name="management">
      <node name="account3" id="act3">
        <property name="username"  value="bgates" />
        <property name="firstName" value="Bill" />        
        <property name="lastName"  value="Gates" />                
        <property name="password"  value="gates1980!" />        
      </node>
    </node>    
    <node name="support">
      <include ref="bo@" />
      <include ref="act3" />
    </node>
  </node>
</registry>

Basics

As can seen, the Regis XML configuration format directly reflects the structure of a registry:

  • A configuration takes the registry root element. Such an element contains one to many node elements, that each correspond to a Node under the registry's root Node.
  • A node element can in turn contain zero to many node elements, as well as zero to many property, link and include elements. Each node element takes a mandatory name (corresponding to the Node's name). Optionally, a node element can take an id attribute, meant to identify that node uniquely within the configuration, so that it can be referred to later on in the XML (the id attribute is not taken into account when the configuration is actually loaded into the registry).
  • In addition, as an alternative to the id attribute when refering to another node, the full path of the referred node under the root can be given (this can be useful when loading configuration from multiple interrelated files, or when loading configuration in a registry that already has nodes needing to be referred to):

    <registry>
      ...
      <node>
        <node name="support">
          <include path="users/backoffice@" />
          <include path="management/account" />
        </node>
      </node>
    </registry>

  • A property element takes mandatory name and value, indicating the name and value of the corresponding property.
  • A link element corresponds to a prepended or appended link. It optionally takes a type attribute, whose value may be prepend or append, depending on the desired type of link (if the attribute is not specified, an appended link is presumed). In addition, the element takes a mandatory ref attribute, whose value corresponds to the id of an existing node element. When instantiating the objects corresponding to the configuration, this indicates to the configuration loader that a node should be added (has a link) at that specific point in the hierarchy.
  • An include element corresponds to a node that is to be included under the corresponding current node. Similarly to the link, its takes a mandatory ref attribute, whose value, this time, corresponds to the node whose id matches the given value and that is to be included, or to the node whose child nodes should be included. Indeed, the value of the ref element can be of the form node_id or node_id@ - with the @ character being interpreted in a special way be Regis. In the former case, the node corresponding to the given identifier is included; in the latter case, all child nodes of the node whose identifier is given will be included (more concretely, the nodes the child nodes will be added to the target node one at a time, as if they would be added one by on individually, through multiple includes).

Resource/File Resolution

All URIs that point to configuration files and are processed by Regis are searched using the same resolution logic:

  1. Search under the "current directory" (corresponding to the user.dir system property).
  2. Search the resource as a URL (if it starts with a scheme handled by the JDK - http, etc.).
  3. Search in the application's classpath.

Search stops as soon as a resource is found. If all search steps have completed without a resource being found, an exception is thrown.

Operations

The configuration allows specifying which operation should be performed, on a per-node basis. This is done by setting the operation attribute on the appropriate node element. The value of that attribute can be one of the following:

  • create: this will trigger the creation of a new node, completely overwriting the current corresponding node in the hierarchy, if any, and deleting all child nodes of the current one.
  • update: this will indicate that the current corresponding node (if any) is to be kept if it exists, as well as its properties, child nodes, links, and includes. In that case the node configuration is simply "added" to the current node. If no current node exists, then the result amounts to the one of a create.
  • update-overwrite: this will indicate that the current corresponding node (if any) is to be kept if it exists (as well as links, children and includes), but its properties deleted prior to the node configuration being "added" to the current node. If no current node exists, then the result amounts to the one of a create. This is convenient when you want to delete "old" properties in order to keep only the ones that are currently needed.
  • delete: this indicates that the matching node is to be deleted - as well as its children.
By default, node operations are done in update mode, meaning that if no operation attribute is specified on a given node, update is presumed.

The configuration below is given as an example of operations on nodes:

<registry>
  <node name="databases">
    <node name="000" operation="delete" />
    <node name="001" operation="update">
      <property name="username" value="mrspock" />
    </node>    
  </node>
  <node name="users">
    <node name="backoffice">
      <node name="account1">
        <node name="address">
          <property name="street"  value="1234 Sesame" />
          <property name="city"    value="New York" />        
          <property name="state"   value="NY" />                
          <property name="country" value="US" />                
        </node>
      </node>
    </node>
  </node>
</registry>

Note that in addition, for convenience, a default operation can be specidied, as an attribute of the registry element:

<registry defaultOperation="update-overwrite">
  <node name="databases">
    <node name="000" operation="delete" />
    <node name="001" operation="update">
      <property name="username" value="mrspock" />
    </node>    
  </node>
</registry>

In such a case, that default operation is "inherited" by all node elements, unless it is overridded at the node level, by specifying the operation attribute.

Loading

In order to load a configuration into a registry, that registry must support read-write operations. Loading a configuration involves using a RegistryConfigLoader:

RWSession session = (RWSession)reg.open();
session.begin();    
RWNode node = (RWNode)reg.getRoot();
RegistryConfigLoader loader = new RegistryConfigLoader(node);
loader.load(new File("path/to/config.xml"));
session.commit();
session.close();

Note that loading a configuration "in" the root node of the registry is not mandatory. One can load a configuration at any existing node in a hierarchy:

RWSession session = (RWSession)reg.open();
session.begin();    
RWNode node = (RWNode)reg.getRoot().getChild("child1/child2");
RegistryConfigLoader loader = new RegistryConfigLoader(node);
loader.load(new File("path/to/config.xml"));
session.commit();
session.close();

The node that is passed to the loader will be considered the "configuration root", but is not necessarily the root of the registry.

Static Interpolation

The configuration supports variables that are interpolated only once, when the configuration file is actually loaded - and prior to be parsed as an XML document. Such variables must follow the $[variable_name] notation. The example below demonstrates how this is done:

<registry defaultOperation="update-overwrite">
  <node name="transactionService">
    <property name="jndiName" value="services/transaction" />
    <property name="domain"   value="$[ubik.jndi.domain]" />
  </node>
</registry>

Expected variables must be passed when loading the configuration, or as system properties:

RWSession session = (RWSession)reg.open();
session.begin();    
RWNode node = (RWNode)reg.getRoot();
RegistryConfigLoader loader = new RegistryConfigLoader(node);
Map vars = new HashMap();
vars.put("ubik.jndi.domain", "mercury");
loader.load(new File("path/to/config.xml"), vars);
session.commit();
session.close();

If given variables cannot be resolved using the given map of values passed in, then the system properties are looked up.

Keep in mind that static interpolation occurs upon loading the XML configuration, and prior to parse the XML. Therefore, static variables are not resolved using links or inheritance. Upon being resolved, static variables appear as being orginally part of the XML configuration.

Static Inclusion

Static includes allow including the content of a given Regis configuration resource into another. The snippets below illustrate how this is done. First, a parent file specifies the resource to include:

<registry defaultOperation="update-overwrite">
  <node name="datasources">
   <staticInclude uri="org/acme/app/jcbc.regis.xml">
     <param name="datasource" value="orders" />
     <param name="driver" value="org.hsqldb.jdbcDriver" />
     <param name="username" value="sa" />
     <param name="password" value="" />
     <param name="url" value="jdbc:hsqldb:hsql://localhost/orders" />
   </staticInclude>
   <staticInclude uri="org/acme/app/jcbc.regis.xml">
     <param name="datasource" value="inventory" />
     <param name="driver" value="org.hsqldb.jdbcDriver" />
     <param name="username" value="sa" />
     <param name="password" value="" />
     <param name="url" value="jdbc:hsqldb:hsql://localhost/inventory" />
   </staticInclude>
  </node>
</registry>

An the included resource would go as follows:

<node name="$[name]">
  <driver>$[driver]</driver>
  <username>$[username]</username>
  <password>$[password]</password>
  <baseUrl>$[url]</baseUrl>
</node>

As the snippets above illustrate, a staticInclude tag allows including a configuration resource as some sort of template, to which variables can be passed (and which are substituted at load time with the proper values). This allows reusing a given configuration resource across different contexts (i.e.: with different values).

Note that the included resource must evaluate to a single configuration object a load time (a property, a node, etc.). In other terms, it means that the included file must correspond to a single XML element corresponding to a configuration object known to Regis.

Parameter Assignment

As was shown in the previous sections, parameters can be based to the configuration loader in the form of a Map instance. These parameters can then be recuperated in the configuration and assigned to nodes, properties, etc. For example:

<registry>
  <node name="myapp">
    <node name="cache">
      <property name="baseDir">
        <value><paramRef name="cacheBaseDir" default="./cache" /></value>
      </property>
    </node>    
  </node>
</registry>

As the example demonstrates, a paramRef element allows assigning a user-defined value to any "object" in the configuration. In this case, the only requirement is to replace attribute notation with element notation (in the above, the value attribute that is normally used on the property is replaced by a corresponding value element).

The paramRef takes the following attributes:

  • name: the name of an existing parameter or system property.
  • default: a default value to be used, if none could be found for the specified name.

Not that if no default value is given and no value could be found for the specified name, an exception is thrown and configuration loading is aborted.

Conditional Loading

The configuration in addition supports conditional instructions allowing to load given parts of the configuration, based on user-specified parameters or given system properties. The instructions are processed only at configuration loading time (i.e.: they are not kept in the registry, and do not impact post-configuration registry operations, such as node and property lookups).

If

The if element allows conditionally processing a single nested element, based on a user-defined parameter or system property:

<registry>
  <if param="environment" value="dev">
    <node name="someNode">
      <property name="foo" value="bar" />
    </node>
  </if>
</registry>

The above node will be created in the registry only if the "environment" parameter value equals "dev". The if element takes the following attributes:

  • param: the name of a user-defined parameter or system property to match
  • value: a value or comma-delimited list of values to test for equality - if a list is specified, the condition evaluates to true on the first value that matches.

If no value is specified, the condition evaluates to true if a value exists (is not null) for the given parameter/system property.

Note that condition evaluation can also be done on a pattern: condition evaluation will be based on a value being matched, rather than on equality:

<registry>
  <if param="domain" value="prod-asia-*">
    <node name="someNode">
      ...
    </node>
  </if>
  <if param="domain" value="prod-americas-*">
    <node name="someNode">
      ...
    </node>
  </if>  
</registry>

Imagine in the above example that a configuration is used by servers that are grouped by domain, geographically, where a given continent may be served by multiple server clusters (or domains). In this case, configuration may vary on a per-continent basis, and configuring nodes on a per-domain basis may be too tedious. To be more precise, imagine that asia is served by the following domains: prod-asia-01, prod-asia-02, etc. Then, all these domains could share common configuration nodes, without having to repeat the nodes for each domain. This feat, as illustrated above, is accomplished through pattern matching.

The matches attribute can support a comma-delimited list of patterns. The condition evaluates to true on the first pattern that matches.

Unless

The unless element allows conditionally processing a single nested element, based on a user-defined parameter or system property:

<registry>
  <unless param="environment" value="dev">
    <node name="someNode">
      <property name="foo" value="bar" />
    </node>
  </unless>
</registry>

The above node will be created in the registry only if the "environment" parameter value does not equal "dev", or is not defined. Therefore, the unless element is the "opposite" of the if element. It takes the following attributes:

  • param: the name of a user-defined parameter or system property to match
  • value: a value or comma-delimited list of values to test for equality - if a list is specified, the condition evaluates to false on the first value that matches.

If no value is specified, the condition evaluates to false if a value exists (is not null) for the given parameter/system property.

This element also supports a matches attribute, for pattern matching, as explained for the if element in the previous section.

Choose

The choose element is similar to the if element, except that it allows testing for multiple conditions, and "branching" on the first condition that is verified:

<registry>
  <choose>
    <when param="environment" value="dev">
      <node name="someNode">
        <property name="foo" value="bar" />
      </node>
    </when>
    <when param="environment" value="prod">
      <node name="someNode">
        <property name="sna" value="fu" />
      </node>
    </when>    
    <otherwise>
      <node name="someNode">
        <property name="fu" value="manchu" />
      </node>    
    </otherwise>
  </choose>
</registry>

The choose element takes one to many when elements, and an optional otherwise element. The when element takes the following attributes:

  • param: the name of a user-defined parameter or system property to match
  • value: a value or comma-delimited list of values to test for equality - if a list is specified, the condition evaluates to true on the first value that matches.

If no value is specified, the condition evaluates to true if a value exists (is not null) for the given parameter/system property.

For its part, the The otherwise element takes no attributes, and is meant as a catch all, provided no "when" condition is satisfied.

This element also supports a matches attribute, for pattern matching, as explained for the if element in a previous section.

Alternate Property Notation

Regis support an alternate way of defining node properties in the XML configuration. Instead of defining the property element with the name and value attributes, one could define a custom element with the element's name being the name of the property and the element's content being the value of the property. As an example, the following defines two nodes with the same properties:

<registry>
  <node name="databases">
    <node name="000">
      <property name="username" value="jsmith" />
      <property name="password" value="foo123" />      
      <property name="url"      value="jdbc://saturn:5432/" />            
    </node>
    <node name="000-custom">
      <username>jsmith</username>
      <password>foo123</password>
      <url>jdbc://saturn:5432/</url>
    </node>
  </node>
</registry>

The alternate property notation is supported in order to offer a less verbose configuration format. However, the custom property definition will only work under the following circumstances:

  • inside a node: the custom element will be recognized as a property only when nested in a node element.
  • no attributes: the custom element must not have any attributes.
  • content as value: the value of the custom property must be the content of the element.
  • no reserved word: the custom element name must not match any reserved element name of Regis.
  • no 'name' property: the custom element name cannot be 'name' otherwise it can conflits with the name of the parent's node (in such a case use an explicit property element).

Programmatic Configuration

In order to be able to programmatically configure a registry, you need an implementation that supports write-operations. In such a case, you must cast your nodes to RWNodes, which gives you access to write-methods. Of course, all operations that modify the state of a registry must be done in a transaction. Transaction demarcation is available by casting your session to a RWSession.

Configuration Beans

Configuration beans consist a nifty feature that allows wrapping Node instances in implementation of user-defined interfaces. Here are the steps involved:

  • Design a Java interface that will act as your configuration interface. The interface must have getter methods that will match the corresponding property on the node that you want to create a proxy over.
  • Design your configuration according to your interface - conversely, the configuration could be designed first, and the Java interface designed based on that configuration.
  • Use the BeanFactory class to create an implementation of your Java interface "over" the configuration node that you target.

Define a Java interface

public interface SessionManagerConf{

  public long getIdleTimeout();
  
  public File getPersistenceDir();
  
}

Define the Configuration

<registry>
  <node name="sessionManager">
    <property name="idleTimeout"     value="500000" />
    <property name="persistenceDir"  value=".sessions">
  </node>
</registry>

Create an Instance of the Configuration Interface

...
  Node node = registry.getRoot().getChild("sessionManager");
  SessionManagerConf conf = (SessionManagerConf)
  BeanFactory.newInstance(node, SessionManagerConf.class);
 ...

When creating the interface instance, Regis performs runtime checks to make sure that a) all getter methods have an existing, corresponding property; b) properties can be converted to the return type of their corresponding getter method.

Supported return types for getter methods consist of primitive types (boolean, int, long, float, double) as well as java.lang.String and java.io.File.

Using the Instance

Keep in mind that every time a getter method is called on a configuration bean, a RegisSession is internally created (and closed after use, of course). This is hidden from the application and occurs within the dynamic proxy:

...
  long    timeout    = conf.getTimeout();
  boolean persistent = conf.getPersistent();  
 ...

In order to optimize session usage you can use the SessionUtil class, like so:

...
  SessionUtil.createSessionFor(conf);
  try{
    long    timeout    = conf.getTimeout();
    boolean persistent = conf.getPersistent();  
	...
  }finally{
    SessionUtil.close();
  }

The above optimization is most important in conjunction with the Hibernate registry.

Registry Server

Regis comes with a registry server that embeds a RegistryExporter class. The javadoc of the RegistryServer class provides all details nessary to configure a server instance.

The server can be started using the regis.sh script (or regis.bat under Windows) unde the bin directory of the Regis distribution. The argument of the script is expected to be the path to the Java properties file to use to initialize and start the server.

For starters, let's just mention the features of the registry server:

  • Relies on Ubik for remoting.
  • Allows remotely uploading configuration files.
  • Multiple registry servers can work in peer-to-peer mode.

Ant Tasks

Configuration Upload

An Ant task is provided to allow uploading configuration files into a registry server (see the javadoc for more information). The example below demonstrates the task's usage (the registry server is presumed to be published in Ubik's JNDI server):

<!-- declaring the taskdef -->
  <taskdef classname="org.sapia.regis.remote.ant.RegistryTask"
    name="updateRegistry">  	
    <classpath>
      <fileset dir="${lib.dir}">
        <include name="**/*.jar"/>
      </fileset>      	      	
    </classpath>
   </taskdef>
      
  <!-- invoking the task -->
  <updateRegistry 
    jndiName="regis" 
    username="regis"
    password="secret"
    nodePath="myapp/database"
    url="ubik://192.168.0.103:1099" 
    config="etc/configDb.xml">
  </updateRegistry>

Code Generation

Loosely-typed configuration may become a pain when dealing with a statically-typed language such as Java. In order to bridge the gap between the loose, tree-based configuration model of Regis and the static nature of Java, a code generation facility has been added to the framework. Basically, it generates classes based on the configuration contained in a Registry.

Generating Code with Ant

Regis provides an Ant task that may be configured as follows:

<!-- declaring the taskdef -->
<taskdef classname="org.sapia.regis.ant.CodeGenTask"
    name="codegen">
  <classpath>
    <fileset dir="${project.distributionDir}">
      <include name="sapia_regis.jar"/>
    </fileset>
  </classpath>
</taskdef>
    
<!-- invoking the task -->
<codegen 
  destdir="${basedir}/codegen/src" 
  properties="etc/codegen/bootstrap.properties"
  packagePrefix="org.sapia.regis.sample"
  version="1.0"
  rootClassName="SampleApp" />

The task takes the following attributes:

  • destdir: indicates the directory where the sources must be generated
  • properties:
  • indicates the Regis boostrap file to use for loading (or connecting to) the Registry instance whose contents will be used for generating the code.
  • packagePrefix: the package prefix to use, which will be inserted into the generated sources (and used when creating the source files).
  • version: an arbitrary version number, also inserted in the generated sources.
  • rootClassName: The name (excluding the package) of the class to use when generating the "root" class (the code generator creates such a root class and corresponding factory to serve as a convenient entry point for applications.

Generating Code with Maven

In a manner similar to the Ant task, code can be generated with Maven. It is a good practice to create a separate Maven module for your configuration, and use the plugin as part of that module. By default the plugin will generate the code in the source directory of that module, where it will be picked up by the compilation phase. Your generated code will thus be made available to applications in a transparent manner through Maven's dependency mechanism. Here's a configuration example (see the Regis sources for more details, it provides a Maven sample module meant to illustrate the plugin' usage):

<build>
    <plugins>
      <plugin>
        <groupId>org.sapia.regis.plugin</groupId>
        <artifactId>sapia_regis_maven_plugin</artifactId>
        <version>3.7</version>
        <executions>
          <execution>
            <phase>generate-resources</phase>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              <bootstrap>${basedir}/etc/bootstrap.properties</bootstrap>
              <packagePrefix>com.acme.config</packagePrefix>
              <outputDirectory>${basedir}/generated</outputDirectory>
              <rootClass>AcmeConfigFactory</rootClass>
              <version>1.0</version>
              <generateGetters>false</generateGetters>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

The plugin takes the following parameters:

  • bootstrap: indicates the Regis boostrap that points to the properties to use for loading (or connecting to) the Registry instance.
  • outputDirectory: indicates the directory where the sources must be generated (defaults to Maven's project.build.outputDirectory property). It is better to leave it to the default, since Maven's compilation phase will pickup the generated sources from there automatically.
  • packagePrefix: indicates the Java package under which the code will be generated (defaults to Maven's project.groupId property).
  • rootClass: the name to use for the configuration factory class that will be generated (defaults to ConfigFactory).
  • version: an arbitrary version number, also inserted in the generated sources (defaults to Maven's project.version) property.
  • generateGetters: indicates if getters should be generated for accessors (as in getName()) or not (as in name()).

Using the Generated Code

The following illustrates how to use the generated code. Note that the code generator creates a root class (and associated interface), that corresponds to the root of the configuration. The generator also creates a factory allowing applications to create an instance of the root class, as illustrated below:

import org.sapia.regis.sample.*;
import org.sapia.regis.*;
import java.util.Properties;
 
...
 
Properties props = new Properties();
  
props.setProperty(RegistryContext.BOOTSTRAP, "etc/codegen/bootstrap.properties");
RegistryContext ctx = new RegistryContext(props);
Registry reg = ctx.connect();
RegisSession s = reg.open();
    
SampleApp appConfig = SampleAppFactory.createFor(reg);
for(DatabasesNode node : appConfig.getDatabases().getDatabasesNodes()){
  System.out.println(node.getUsername() + ", " + node.getUrl());
}

s.close();

Behind the Scenes

The code generator creates a class hierarchy correspdonding to the node hierarchy of the Registry instance that is used as a base for code generation. The generator creates packages that match the different node paths under the configuration tree.

Instances of the generated classes encapsulate their corresponding node at runtime. Therefore, the configuration is not hardwired into the source; the generated classes serve as proxies - sort of a strongly-typed abstraction layer.

Spring Integration

Regis comes with a BeanPostProcessor that will process Regis-specific annotations allowing you to inject Regis-based configuration into your classes. The following are supported:

  1. Injecting Node instances into a bean.
  2. Injecting a Regis-generated class instances into a bean (this allows working in a more strongly-types fashion).
  3. Injecting the properties of a generated class instance to fields or members of a bean (i.e.: as Spring bean properties).

The Lookup annotation supports the first two scenarios; the NodeType annotation in turn support the last.

In addition, the processor will also automatically assign the Registry instance it creates to the any field or setter whose type corresponds to the Registry interface.

There is a sample module as part of the Regis source tree that provides a concrete example of the integration with Spring.

Configuring the RegisAnnotationPostProcessor

The first step in order to integrate Regis into a Spring-based application is to configure the RegisAnnotationPostProcessor. The processor takes a single property, which corresponds to the Regis bootstrap. Here's a sample configuration (in which the bootstrap points to the bootstrap.properties file in the classpath):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="regis" class="org.sapia.regis.spring.RegisAnnotationProcessor">
    <property name="bootstrap" value="bootstrap.properties" />
  </bean>

</beans>

The processor instrospects the field and setter methods of your Spring beans, looking for the following annotations:

How the annotations are interpreted is explained in the following sections.

Injecting a Node

To inject a Node instance into a Spring bean, you use the Lookup annotation, as shown below:

@Lookup(path="path/to/node")
private Node config

or:

@Lookup(path="path/to/node")
void setConfig(Node config){
  this.config = congig;
}

You must then specify the path attribute, which corresponds to the path to the node you want to inject.

Injecting a Generated Class Instance

If you're using the code generation facility (which creates a class for each node), you may inject the instance of a generated class, again using the Lookup annotation, but this time without supplying a path. In this case, it is important that the field (or paramter of the setter method) to which you inject the instance have the same type as that instance's generated class.

:

@Lookup
private ConnectionConfig connectionConfig

or:

@Lookup
void setConnectionConfig(ConnectionConfig config){
  this.config = config;
}

Injecting the Properties of a Generated Class Instance

It might be more convenient to simply inject the properties of a given node directly to the properties of your Spring beans. This can be done in the following way

@NoteType(type=com.acme.config.DatabaseConfig.class)
public class DatasourceServiceImpl{

  @Prop
  private String url;
  
  @Prop(name="user")
  private String username;
  
  private String password;  
  
  @Prop(optional=true)
  private int poolSize;
  
  @Prop(defaultTo="30000")
  private long idleTimeOut
  
  private Pool<Connection> connections; 
  ...
  
  @Prop
  void setPassword(String password){
    this.password = password;
  }
  
}

The NodeType annotation takes the Regis-generated class whose instance should have its property values injected into the Spring bean. As illustrated, the Prop annotation is used to indicate which fields/methods of the bean will be targeted by the injection. Property resolution follows the following rules

  • If the Prop annotation's name attribute is not specified, a property whose name matches the name of the field or setter method will be looked up - in the case of the method, the setter name will be truncated from its set prefix, and the remaining string's first letter will be decapitalized.
  • If the Prop annotation's name attribute is specified, it's value will be used to retrieve the corresponding property.
Of course thus injected properties will be automatically coerced to the type of their corresponding field (or setter's parameter). Conversion is supported for:
  • All Java primitive types.
  • Enums.
  • Dates whose configured string value respects the following pattern: yyyy-MM-dd hh:mm:ss z

By default, if no property value could be found matching a Prop annotation instance, an exception is thrown. This can be bypassed by setting the optional attribute to true, as illustrated above (the attribute's value is false by default).

Using the defaultTo attribute in turn allows specifying a default value in case no property value could be found - also bypassing the throwing of an exception, as in the case of the optional attribute.

It is not mandatory to specify a Prop annotation. In this case it will be attempted to look up a property that matches the field or setter's name, and if none could be found no exception will be thrown. For such implicit injection to be supported, the auto attribute of the NodeType annotation must be specified. The following example demonstrates the above:

@NoteType(type=com.acme.config.DatabaseConfig.class, auto=true)
public class DatasourceServiceImpl{

  private String url;
  
  private String username;
  
  private String password;  
  
  private Pool<Connection> connections; 
  ...
  
  void setPassword(String password){
    this.password = password;
  }
  
}

Injecting the Registry

As was mentioned previously, if your Spring bean has a field or setter method whose type corresponds to the Registry interface, it will automatically assign to it the registry it creates. Therefore, the registry field of an instance of the class below will have been initialized to a Registry instance when the afterPropertiesSet() method is called:

public class DatasourceService implements InitializingBean{

  private Registry registry;
  
  ...
  
  public void afterPropertiesSet(){
    ...
  }

}

Implementing your Own Type Converters

Internally, the RegisAnnotationProcessor uses built-in implementations of the Converter interfaces to convert string values to actual Java types. You could code your own implementations and register them with the processor (see the interface's javadoc for details, it's quite simple). The register your implementation as follows (the processor takes a list of such converters):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="myConverter" class="com.acme.config.MyConverter" />

  <bean id="regis" class="org.sapia.regis.spring.RegisAnnotationProcessor">
    <property name="bootstrap" value="bootstrap.properties" />
    <property name="converters">
      <list>
        <ref local="myConverter"/>
      </list>
    </property>
  </bean>

</beans>

The converters are processed in sequence, in the order in which they were added, as part of a chain of responsability. Note that that the converters that you add have precedence over the built-in ones - allowing you to overridde their behavior should you require it.

Conclusion

Regis can be used to implement a robust, performant, persistent and distributed configuration infrastructure.