An Implementation will generally require access to a configurable set of parameters to avoid the hard coding of customizable data. A good use of a Configuration is to supply the Implementation with parameters such as pathnames set by a systems integrator before runtime. A poor use of the Configuration is to supply the Implementation with runtime parameters through the Interface as a means of shuttling out-of-band information from an Application
I will present an alternate way of managing Configurations that scale well with large numbers of Implementations and keep to the Interface boundary philosophy. First, we need to understand the OsidLoader.
The OsidLoader is responsible for finding the
Implementation, loading its Configuration, and passing a Context. The
Configuration is comprised of two parts: a properties file found in
the CLASSPATH, and an additional properties set passed into the
OsidLoader from the Application. The loaded properties and additional
properties are combined and assigned to the Implementation via the
Manager:
InputStream is = managerClass.getResourceAsStream(managerClassName +
".properties");
if (null != is) {
configuration = new java.util.Properties();
configuration.load(is);
}
if (null != additionalConfiguration) {
java.util.Enumeration enum = additionalConfiguration.propertyNames();
while (enum.hasMoreElements()) {
java.io.Serializable key = enum.nextElement();
if (null != key) {
java.io.Serializable value = additionalConfiguration.get(key);
if (null != value) {
configuration.put(key, value);
}
}
}
}
manager.assignConfiguration(configuration);
There are several downsides to this. The OsidLOader is
responsible for specifying how a Configuration is managed. It expects
a separate properties file for each Implementation to be accessible in
the CLASSPATH. Although it optimizes for flexibility, for large
numbers of implementations this can be tedious. It also provides a
means for the Application to shuttle properties into the
Implementation and this is a big no-no. But the
The OsidManager, however, is an interface left to be
defined by the implementation. Most implementations of
OsidManager I have seen simply stash the Configuration
supplied by the OsidLoader and make it accessible to its
children.
public void assignConfiguration (java.util.Properties configuration)
throws org.osid.OsidException {
this.configuration = configuration;
return;
}
Even though we can't mess on the OsidLoader side of
the equation, we can do whatever we like on this side. So, lets have
some fun.
The first thing to do is close a road to uninteroperability. Now,
when assignConfiguration() is invoked, the supplied
Configuration is ignored and instead fetches its own.
public void assignConfiguration(java.util.Properties configuration)
throws org.osid.OsidException {
loadConfiguration();
return;
}
What does loadConfiguration() do? The answer is
hatever you like. The Configuration is specific to an
implementation. If there is an implementation that wants to make
remote procedure calls to a configuration database, so be it. Although
I'll keep things simple and use a java.util.Properties
compatible file. With a twist.
For a collection of Implementations, it is more convenient to have a single properties file and edit the parameters all in one place. At other times it's handy to be able to create a more specific properties file to override the central configuration. Both are useful so let's do both.
The first thing to do look for a general osid.properties configuration file in the CLASSPATH. If that exists then the properties contained inside are loaded.
protected void loadConfiguration()
throws org.osid.OsidException {
this.configuration = new java.util.Properties();
try {
String file = ClassLoader.getSystemResource("osid.properties").getPath();
if (file != null) {
java.io.InputStream is = new java.io.FileInputStream(file + ".properties");
this.configuration.load(is);
is.close();
}
} catch (Exception e) {
// ignore and continue, these are optional
}
...
Now we want to load properties for a more specific Implementation, or should that be an Implementation Suite, or perhaps a given OSID? We don't know, so let's search for them all.
For example, for an Implementation with the name of
com.ontapsolutions.osids.authentication.kerberos we will search
for files in the following order so that in the case of conflicting
property names, precedence goes to the more specific rather than the
more general properties file.
It's not reasoanble to make use of all of these files at the same
time. This little game of strings provides the integrator with the
flexibility to design their property scheme while still allowing
instances of local configuration. Here's the code for
loadConfiguration().
protected void loadConfiguration()
throws org.osid.OsidException {
String className = getClass().getName();
java.util.Vector elements = new java.util.Vector();
int index = 0;
String app = (String) getOsidContext().getContext("context");
if (app != null) {
className += "." + app;
}
while (index != -1) {
index = className.indexOf(".", index == 0 ? 0 : index + 1);
if (index != -1) {
String token = className.substring(0, index);
elements.add(token);
}
}
elements.add(className);
this.configuration = new java.util.Properties();
try {
String file = ClassLoader.getSystemResource("osid.properties").getPath();
if (file != null) {
java.io.InputStream is = new java.io.FileInputStream(file + ".properties");
this.configuration.load(is);
is.close();
}
} catch (Exception e) {
// ignore and continue, these are optional
}
int size = elements.size();
for (int i = 0; i < size; i++) {
try {
String file = ClassLoader.getSystemResource(elements.elementAt(i) + ".properties").getPath();
storeConfiguration(file);
} catch (Exception e) {
// ignore and continue, these are optional too
}
}
return;
}
So, you may have noticed the real work was offloaded to another
method. The properties cannot simply be loaded as specified in a
properties file since they all come together in the same
Properties table. They need to be separated by making
sure the property name includes full pathname to the class, or does
not exceed the scope of its own path. A Properties file cannot
introduce properties that effect other implementations outside of it's
own tree.
For a properties file of
edu.mit.osid.repository.properties:
| property | becomes | ||||
|---|---|---|---|---|---|
| dbuser | edu.mit.osid.repository.dbuser | ||||
| edu.mit.osid.repository.ocw.url | edu.mit.osid.repository.ocw.url | ||||
| edu.mit.osid.imageFile | not allowed |
Here is a code listing which implements this behavior:
private void storeConfiguration(String file) {
java.util.Properties p;
try {
java.io.InputStream is = new java.io.FileInputStream(file + ".properties");
p = new java.util.Properties();
p.load(is);
is.close();
} catch (java.io.IOException ie) {
return;
}
java.util.Enumeration keys = p.propertyNames();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String value = p.getProperty(key);
String prefix = null;
int i = key.lastIndexOf('.');
if (i != -1) {
prefix = key.substring(0, i);
}
if (prefix == null) {
/*
* Unqualified keys get the filename prepended.
*/
key = file + "." + key;
} else if ((file.startsWith(prefix) && !file.equals(prefix))) {
/*
* keys cannot exceed the scope of their file
*/
System.err.println("error in properties file: " + file);
System.err.println("property " + key + " is out of scope");
continue;
} else if !((file.equals(prefix) || prefix.startsWith(file))) {
System.err.println("error in properties file: " + file);
System.err.println("property " + key + " is broken");
continue;
}
this.configuration.setProperty(key, value);
}
return;
}
The next step is providing the means for a manager to access its
Configuration. Instead of leaving the Properties object
accessible to the manager subclassing this OsidManager,
there's more work to do in implementing the cascade.
If, for example, edu.mit.osid.logging.LoggingManager
needs to identify a property named log_file, the
properties should be searched in the following order giving precedence
to more specific properties.:
The subclasses of OsidManager acquire their properties
through the getConfiguration() method.
protected String getConfiguration(String key) {
if (key == null) {
return (null);
}
org.osid.OsidContext context = null;
String className = getClass().getName();
java.util.Vector elements = new java.util.Vector();
String value;
int index = 0;
try {
context = getOsidContext();
} catch (org.osid.OsidException oe) {
// skip it
}
if (context != null) {
try {
String app = (String) context.getContext("context");
if (app != null) {
className += "." + app;
}
} catch (org.osid.OsidException oe) {
/* ignore it */
}
}
if (this.configuration != null) {
value = this.configuration.getProperty(className + "." + key);
if (value != null) {
return (value);
}
String path = className;
while (index != -1) {
index = path.lastIndexOf(".");
if (index != -1) {
path = path.substring(0, index);
value = this.configuration.getProperty(path + "." + key);
if (value != null) {
return (value);
}
}
}
}
return (null);
}
In any OSID implementation, properties cannot be accessed until
after the OssidLoader instantiates the manager and the
OsidLoader invokes assignConfiguration().
This leaves the manager to perform a lazy retrieval of properties or
to check for a properties initialization flag in each and every
method. Or, this can also be made a problem of the
OsidManager.
public void assignConfiguration(java.util.Properties configuration)
throws org.osid.OsidException {
loadConfiguration();
initialize();
return;
}
Here's an example of what it looks like on the manager side:
public class AuthenticationManager
extends OsidManagerWithCascadingProperties
implements org.osid.authentication.AuthenticationManager {
/*
* properties
*/
private String password_file = null;
private int retries = 3;
/**
* Constructs a new AuthenticationManager.
*/
public AuthenticationManager() {
super();
}
/*
* initialization method
*/
protected void initialize()
throws org.osid.OsidException {
String property;
/*
* initialize properties. See javadoc for detail.
*/
property = getConfiguration("password_file");
if (property != null) {
this.password_file = property;
}
property = getConfiguration("retries");
if (property != null) {
try {
this.retries = Integer.parseInt(property);
} catch (NumberFormatException nfe) {
// ignore error, for now
}
}
return;
}
....
All of these Property file lookups are performed through
ClassLoader.getSystemResource(). This adds a new
dimension of flexibility with regard to the locations of these
files. The packager should tread wisely.
It's also worth re-emphasizing that properties are specific to a given implementation. Suites of Implementations may be packaged that share some property names in common but there's no requirement that a property name be standardized among Implementations. Such standardization would largely defeat the purpose of Implementation tuning.
This example not only illustrates a different scheme of Configuration management, but also demonstrates that although an Interface inherently restricts what can be passed through it, the job can be done by pushing more of the workload to the implementation.