Configuration System Documentation
Documentation on how to use the ConfigurationManager and associated classes.
Overview
The ConfigurationManager configuration system is a stand-alone configuration system meant to be used with the Kepler module system. The requirements the system was built around can be found here. The main goals of this system are to provide an easy-to-use API for classes within a module to both get and set configuration properties. Each module can control its own properties, but other modules can overwrite those properties or add to them for their own needs. A listener API allows for external classes to be notified on change events. Modules can prevent their properties from being overwritten by other modules at runtime by declaring any property to be non-mutable.
Any configuration files that are in a module's resources/configurations directory will be automatically read in by the ConfigurationManager at Kepler startup. The name of the configuration file becomes the namespace to which its properties belong by default. You can also set the namespace in the configuration file by setting a <namespace> element directly under the root. If you try to add a property from one namepace to a property of another namespace, you'll get an exception. Properties within a tree must be of the same namespace. Overrides are an exception. More about those below.
After Kepler starts up, all of the configurations stored in resources/configurations are available to any module for reading. Properties that are mutable (default) are available for writing. The ConfigurationManager is the main gateway by which to access the root level ConfigurationProperties. Once you obtain a ConfigurationProperty for the module/namespace that you want, it can be manipulated independently without the use of the ConfigurationManager. However, if you want the property serialized, the ConfigurationManager will need to be instructed to save the configuration.
ConfigurationManager
The ConfigurationManager (org.kepler.configuration.ConfigurationManager) is a singleton instance which allows any class in Kepler to access configuration information loaded in the system. The main type of object the ConfigurationManager returns is a ConfigurationProperty, which represents a single configuration element in the system. A ConfigurationProperty can be composed of a name/value pair and/or nested properties.
The structure of the configuration itself is a tree, with the root being the module that owns the ConfigurationProperty. For instance, if I want to access the property "splash/image" which is stored in the "common" module, I would first get an instance of the ConfigurationManager, then ask it for the common module's configuration.
ConfigurationProperty commonProperty = ConfigurationManager.getInstance() .getProperty(ConfigurationManager.getModule("common"));
Note that the configuration returned here is stored, by default, in common/resources/configurations/configuration.xml. The namespace for this default module configuration is "configuration". When working with the default namespace, the namespace information can be ommitted from the API calls.
Once we have the commonProperty, we can now dig down into the structure of it with various methods. You can always see the structure for debugging purposes with the ConfigurationProperty.prettyPrint() method. There is also a static method, ConfigurationProperty.prettyPrintList() that will print a List of ConfigurationProperties nicely. Here's an example of what our commonProperty might look like with prettyPrint():
config splash image = images/kepler-splash.png jar-dir = actors/lib/jar/ autoDataSourcesUpdate delay = 0 value = true log_file = false viewPaneTabPanes viewPane name = Kepler Classic viewPaneLocation name = W tabPane name = Components tabPane name = Data tabPane name = Outline viewPaneLocation name = E .....
The main methods we use to access the structure of the commonProperty are the various getProperty() calls and getProperties() calls. getProperty() will always return a single ConfigurationProperty based on the parameters you give it. getProperties() will always return a List (java.util.List) of properties based on the parameters you give it. If you want to find a List of properties that match name/value pair constraints, use the findProperties() methods.
To get the "splash/image" property from our commonProperty, we would do this:
ConfigurationProperty splashScreenProperty = commonProperty.getProperty("splash.image"); //note that the root element is omitted. For ease of typing, the root element //does not need to be included in the dot notation path.
To get the actual value, use the getValue() method:
String splashScreenValue = splashScreenProperty.getValue(); //splashScreenValue == "images/kepler-splash.png"
Note that you can use a dot notation to symbolize nested values. This works in any of the getProperty() or getProperties() methods.
You can also just use the ConfigurationManager to get this property in one call:
String splashScreenValue = ConfigurationManager.getInstance() .getProperty(ConfigurationManager.getModule("common"), "splash.image") .getValue();
Finding ConfigurationProperties
ConfigurationProperty includes several findProperties() utility methods. These methods make it easy to find properties within a configuration that have a specific name/value pair. For instance, from our configuration above, we might want to find the viewPaneLocation with a name of "W". Here's the code that would get that property:
//get a list of the viewPaneLocations List viewPaneLocationList = commonProperty.getProperties("viewPaneLocation", true); //find the property in that list with the name="W" ConfigurationProperty viewPaneLocationWProp = (ConfigurationProperty) ConfigurationProperty.findProperties(viewPaneLocationList, "name", "W").get(0);
The findProperties() method can also be used to findProperties recursively in a single property. See the API docs for more information.
Namespaces
Namespaces allow modules to include more than one configuration file. For instance, a developer might want to have a configuration file for GUI elements and a configuration file for database elements. You access these files using the same get ConfigurationManager.getProperty() method as above. Namespaces keep configuration properties separated from one another. You cannot import or overwrite a ConfigurationProperty from one namespace with one from a different namespace without specifying that you want to override the namespace. If you try, a NamespaceException will be thrown.
The namespace of a ConfigurationProperty is, by default, its serialization filename without the extension (i.e. if the filename is "props.xml", the namespace would be "props"). This can be changed by adding a <namespace> element below the root of your configuration file. For example, if props.xml looks like this:
<?xml version="1.0"?> <config> <namespace>new namespace</namespace> <prop1> ... </prop1> </config>
The namespace will be set to "new namespace" instead of "props". If you have more than one file in your module's resources/configurations directory with the same namespace element, the properties for those files will be merged under one namespace in the ConfigurationManager.
Example Files:
Props1.xml: <config> <namespace>test-ns</namespace> <prop>value 1</prop> </config> props2.xml: <config> <namespace>test-ns</namespace> <prop>value 2</prop> </config>
These files will have the following structure in the ConfigurationManager:
test-ns config prop = value 1 config prop = value 2
value 1 can be accessed with the call
CofigurationNamespace ns = new ConfigurationNamespace("test-ns"); List l = ConfigurationManager.getProperties(module, ns, "config.prop"); String prop1Val = ((ConfigurationProperty)l.get(0)).getValue();
Configuration File Format (Serialization)
The ConfigurationManager has the potential to deal with any number of different serialization formats through its ConfigurationReader and ConfigurationWriter interfaces. The default reader/writer is for XML based configuration. Any arbitrary schema can be used. The one caveat is that attributes are not currently supported, so all configuration data must be stored in XML elements. Any element can have a <mutable> property within it. The system will interpret that into the ConfigurationProperty and it will not be stored as data. If mutable is set to false, that property may not be changed at runtime.
Any changes to the configuration at runtime are not serialized back to the original configuration file. Instead, changed configuration objects are stored in the .kepler/configuration directory. The changes are the read in over top of default values upon a deserialization.
Overriding Configuration Properties
ConfigurationManager includes a method for overriding configuration properties from other modules. This enables a module author to only override specific properties of the configuration instead of overriding entire config files. Overrides can be performed in the org.kepler.module.<modulename>.Initialize (implements org.kepler.module.ModuleIntializer) class of the module (which gets called automatically by the KeplerIntializer at startup. See the workflow-run-manager module for an example. The example looks like this:
//get the property you want to override ConfigurationProperty commonProperty = ConfigurationManager.getInstance() .getProperty(ConfigurationManager.getModule("common")); //get the property you want to use to override ConfigurationProperty wrmProperty = ConfigurationManager.getInstance() .getProperty(ConfigurationManager.getModule("workflow-run-manager")); //we need to override the common tab pane properties with the ones from wrm ConfigurationProperty commonViewPaneTabPanesProp = commonProperty.getProperty("viewPaneTabPanes"); ConfigurationProperty wrmViewPaneTabPanesProp = wrmProperty.getProperty("viewPaneTabPanes"); commonProperty.overrideProperty(commonViewPaneTabPanesProp, wrmViewPaneTabPanesProp, true);
Configuration Customization For Different Users
If several users are sharing an installation, their configurations can be stored in their $HOME/KeplerData directory without affecting the other configurations. When the ConfigurationManager goes to read a property, it looks in the $HOME/KeplerData location first and if there is a property stored there, it uses that instead of what is in the default configuration file. So if one user wants to customize configuration only for himself/herself, he/she can modify or create configuration.xml at $HOME/KeplerData/modules/<module>/configuration directory. The configuration.xml rather than the default one for the module will be loaded by ConfigurationManager and these customized configurations can be progmatically gotten by ConfigurationManager.getProperty() APIs.
Internationalization Support
ConfigurationManager includes built in support for i18n. If you have a configuration file that is for a specific locale, name it with the appropriate ending (i.e. "_en_US"). ConfigurationManager will automatically load the correct configuration file for the current Java locale setting. The default is _en_US, so any files without a locale ending are defaulted to be US English. If a different locale is set, but there is no file with the correct name, the default file will be loaded. See the unit tests for the module configuration-manager for an example.
Overriding Configuration Properties from a Custom Module at Startup Time
The configuration manager can be used to change or override configuration values at startup. All modules can contain the class org.kepler.module.<module_name>.Initialize which should implement the org.kepler.module.ModuleInitializer interface. If a module contains this class, the module's Initialize.initializeModule() method will be called upon Kepler startup. Each module's initializer is called in the order in which the module resides in the module.txt file for the current suite.
Within initializeModule(), configuration properties can be added, removed or overridden. A common example is to add a new frame to the GUI. See https://code.kepler-project.org/code/kepler/trunk/modules/reporting/src/org/kepler/module/reporting/Initialize.java for an example.
Additional Features and Notes
- Dirty flag: anytime a ConfigurationProperty is changed, its "dirty" flag is set. You can test if a property has changed since the last time it was serialized with the isDirty() method.
- Event Listeners: ConfigurationManager allows you to register change listeners that are fired anytime a property is changed.
- getParent(): You can get the parent property of any property with the getParent() method.
- Unit tests: the configuration-manager module has a full battery of unit tests. These are good examples for using the various functions of ConfigurationManager.
Possible Additions
- Right now, you cannot use the shortcut dot naming convention with the findProperties() methods. The reason is that it would require an index on each of the subelements you are searching. This is entirely possible, just not implemented. So, for now, you cannot use the dot name shortcuts with findProperties().
- Reduce casting by subclassing List. Casting (ConfigurationProperty) is kind of tedious and could be eliminated by having the list methods return a subclass of List that returns a ConfigurationProperty from its get() method.
- Allow a configuration file to define its namespace name instead of using the filename for the namespace.
Links