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 basic (yet useful) admin GUI (click to enlarge):
|
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.
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();
...
|
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:
- 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.
- 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.
- 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).
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. |
Parameter Assignment
As was shown in the previous section, 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>
|
Configuration Testing
Regis provides an Ant task allowing to test a configuration prior to loading it into
a registry (see the javadoc
for more information). The example below illustrates how to use the task:
<!-- declaring the taskdef -->
<taskdef classname="org.sapia.regis.remote.ant.CheckConfigTask"
name="checkConfig">
<classpath>
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
</classpath>
</taskdef>
<!-- invoking the task -->
<checkConfig
properties="conf/regis.properties">
<checkNode path="acme/app/databases">
<checkProperty name="url" value="jdbc:postgresql://acme/database" />
</checkNode>
<checkNode path="acme/app/users/charliebrown">
<checkProperty name="username" value="cbrown" />
<checkProperty name="password" value="foobar1234!" />
<!-- checking sub-node of parent -->
<checkNode path="permissions">
<checkProperty name="read" value="true" />
<checkProperty name="write" value="false" />
<checkProperty name="execute" value="false" />
</checkNode>
</checkNode>
</checkConfig>
|
Running the Admin Client
The project's Ant build file contains the following snippet
that is invoked in order to run the admin client:
<target name="admin">
<java classname="org.sapia.regis.gui.Main">
<classpath>
<path refid="lib-classpath" />
<fileset dir="${project.distributionDir}">
<include name="sapia_regis.jar"/>
</fileset>
</classpath>
</java>
</target>
|
You can thus invoke the admin target from the command-line and it
will launch the client. Initially, you are prompted for the username,
password and all other parameters necessary to connect to a remote registry.
The JNDI name can be omitted if the
remote registry is listening on an explicit port. In that case, the given
port will be used to connect directly to that registry. Otherwise,
the port will be expected to correspond to the remote Ubik
naming service, which in turn must hold a stub to the remote registry
under the specified JNDI name.
If you leave all fields empty, the client will be started with an
embedded local registry whose content will be transient. This allows
experimenting with configuration.
Note that the GUI's first intented use is for basic
post-deployment configuration updates
(which makes sense when using persistent configuration). It does not
support editing links, includes, etc. This can be worked around by using
the "Load to Root" functionality, which allows uploading a configuration file at the root
of the remote registry.
Conclusion
Regis can be used to implement a robust, performant, persistent and
distributed configuration infrastructure.
|