A Common CCI architecture
In Part 1 of this essay, we quickly rejected Properties as a general purpose configuration mechanism. In this second part, we have seen that J2EE deployment descriptors and the Preferences API are both much better solutions. However, neither is perfect and the two are currently incompatible. In this section I will sketch the initial outline of a common CCI architecture. The design goals are as follows, in order of priority:
Systematize support for structure, lookup, scope, and metadata.
Preserve the advantages of J2EE deployment descriptors and the Preferences API.
Pay as you go--the CCI infrastructure should be layered so that you only load the features that you need.
Build nothing new--leverage existing code wherever possible.
Integrate with existing code with minimal changes.
Note that this design is not intended as a final answer, but as a starting point for debate.
A first cut at a CCI design
1. Generalize the Preference API's notion of scope to hook in arbitrary providers. The Preferences API defines two scopes: system and root. In order to extend this to support arbitrary scopes, one could add a factory method with a String scope parameter. In this scheme, the new PreferencesFactory interface would look like this:
public interface PreferencesFactory { public Preferences systemRoot(); public Preferences userRoot(); public Preferences root(String scope); }
Scopes should be named using standard Java package naming conventions. Using this approach allows plugin providers to support legacy configuration schemes. The following table shows some possible scope names and their purposes
Table 1. Some possible preference scopes.
Scope | Purpose |
java.lang.util.prefs.User | User scope from current Preferences API |
java.lang.util.prefs.System | System scope from current Preferences API |
javax.servlet.Webapp | accesses web.xml |
javax.naming.Properties | accesses the eccentric search path for jndi.properties files |
javax.security.Properties | accesses the security.properties file |
The systemRoot and userRoot methods are no longer strictly necessary, but are retained for compatibility.
2. Make XML a first class citizen in the Preferences API. In the world of component configuration, both name/value pairs and XML documents are here to stay. The Preferences API already supports the former, and it would be simple to add methods to make XML a first class citizen as well:
package java.util.prefs; import org.w3c.dom.*; public abstract class Preferences { public abstract String get(String key, String def); public abstract Document getDocument(String key, Document default); public abstract void put(String key, String value); public abstract Document putDocument(String key, Document value); //etc. }
3. Explicitly permit some backing stores to be read-only. The Preferences API allows for programmatic write access to configuration. However, this will not make sense for all implementations. For example, consider JNDI properties. As you may remember from Part 1, JNDI properties have a long search path:
JAVA_HOME/lib/jndi.properties
any jndi.properties file visible to the current class loader
system properties, either passed on the command line or set using java.lang.System
properties explicitly passed to the InitialContext constructor
for Applets, properties set in AppletContext
provider resource files
If an application attempts to write JNDI properties using the Preferences API, it is unclear to which location the properties should be written. The simplest way to deal with this problem is to disallow write access when accessing JNDI properties through the legacy javax.naming.Properties scope.
There are two ways to implement this prohibition: either create a ReadOnlyStoreException subclass of BackingStoreException for providers that are read-only, or simply throw a BackingStoreException with an appropriate detail message.
4. Add metadata support to the Preferences API. None of the approaches to CCI that we have discussed thus far have any programmatic support for metadata. This is perhaps the single biggest weakness of CCIs in comparison with their better known cousins, APIs. To remedy this, the Preferences API should include a programmatic interface to enumerate CCI metadata, i.e. the possible configuration settings exposed by a package or class. Here is one possible approach:
package java.util.prefs; public interface PreferenceMetadata { public Iterator getKeyNames(String packageOrClass); public Document getSchema(String keyName); }
The getKeyNames method returns an iterator over all the configuration settings that are used by a particular class or package. The getSchema method returns an XML schema that describes the values that a particular configuration setting can take. (Note that for backing stores that use simple primitive types instead of XML, getSchema still be used--the schema returned will just be trivial.)
It is easy to design the PreferenceMetadata interface. The hard question is "Where will this information come from?" There are four reasonably obvious ways to discover this information:
A tool could analyze the bytecodes of compiled classes to locate calls into the Preferences API, and infer most of this information from the method calls used. This is ideal in the sense that the metadata will by definition always be in sync with the code. The disadvantage is having to write the tool.
Store configuration metadata in a well-known file inside a JAR file. This requires that a developer generate and maintain the metadata, and that code always be deployed in JARs.
Store configuration metadata using the new Custom Attributes in Java Specification (JSR 175). This is very similar to the previous option except code no longer needs to be deployed in JARs. On the other hand this approach depends on a cool new technology that doesn't exist yet.
Developers could implement the PreferenceMetadata interface directly, and make it accessible to the Preferences library be registering it with a factory method.
There is nothing wrong with using a hybrid of all four approaches. Also, note that this is pay-as-you-go. If you do not want to make this information available to users of your components, you do not have to.
5. Provide an auditing mechanism to track where configuration information comes from. The first design point, above, defines an extension point to hook existing configuration mechanisms. This is great on the application development side, since all configuration information enters through the same API. However, it doesn't ease the learning curve for application deployers. Deployers still need to learn each and every legacy configuration scheme.
More specifically, deployers are looking for a solution to the following all-too-common problem:
Application does not run correctly.
Change configuration setting in file A.
Application still does not run correctly, and seems to be ignoring configuration setting in file A.
Bang head on wall for a while.
Discover obscure bit of documentation to the effect that "Settings in File B override settings in File A if File B is on the classpath and has the 'override' attribute set."
One (flawed) approach to solving this problem would be to define and enforce a single model for for temporal and spatial scope. Then deployers could learn that one model and be done. The problem with this approach is that different components have different configuration needs. Some components can take all of their settings from one location. Others need to merge settings from several different locations. Similarly, different components will cache different settings for different lengths of time.
Rather than trying to create a shared model for all providers, it is more appropriate to simply require that providers tell the developer what they are doing. A configuration trace is the CCI equivalent of a stack trace to tell us where the configuration actually came from:
package java.util.prefs; public abstract class Preferences { public void printConfigurationTrace(String key); //etc. }
The printConfigurationTrace method simply provides a provider-specific trace of the "locations" consulted in deriving a particular piece of configuration data, and the times these locations were consulted. Here is a hypothetical example of printConfigurationTrace output.
javax.naming.factory.object not found in file jndi.properties (read Mon Jul 15 13:22:37 EDT 2002) merged from file c:/halloway/code/jndi.properties (read Mon Jul 15 16:06:15 EDT 2002) merged from file c:/halloway/shared/jndi.properties (read Mon Jul 15 16:06:15 EDT 2002)
Using this information, a deployer can quickly determine which locations are being consulted for configuration information. Of course, producing this output is more work for a component provider. This is inevitable. Only the component provider has this information. Failing to publish it is like refusing to publish the API used to call into a component.