Develop configurable software applications with ease

Configure a Java SE application using the Obix Framework

Developing easily configurable software is of paramount importance in today's business environment. Software applications are no longer judged simply by the amount of business logic that they encapsulate; they are also judged by how easy they are to maintain. The ability to alter software behavior, via configuration, forms an important aspect of this maintenance cycle.

While the Java language provides a number of features, such as property files and resource bundles, to aid configuration, these lack the features required for today's dynamic business environments. Many Java standards, tools, and containers already utilize more advanced and custom XML configuration formats.

The Obix Framework is an open source framework that provides the common means and formats for storing configuration data in XML, and for accessing this data via simple Java objects. It enables the modularization of configuration data by allowing configuration files to be imported and included in each other, and by organizing configuration information into "modules."

In addition, it supports "hot" configuration amendments—through auto-detection and auto-reload of changes to configuration data—and also provides support for the Java Naming and Directory Interface API (JNDI). Furthermore, it can be integrated into Java applications in numerous ways, including through Java Management Extensions (JMX) and Java Platform, Enterprise Edition listeners that do not require coding, as well as plain Java classes that can be invoked directly. Finally, the framework provides an easy-to-use plug-in API that lets developers extend it to perform initialization-related tasks. This API has been used by the Obix team to provide initialization utilities for other open source frameworks such as Apache's log4j, Hibernate, and Commons DBCP (database connection pools).

In this tutorial, I describe a hypothetical scenario that requires configurable software and for which we create skeletal applications using Obix. The first example provides the closest thing to a "Hello World"-style proof of concept, while the second and third extend this application to showcase less trivial aspects of configuration.

Please note that all the code samples in this article are packaged as an archive, which can be downloaded via the link provided in Resources.

Problem scenario

Valuing financial assets such as stocks or options sometimes involves simulating the price of the asset thousands of times, and taking the average of these values—in the belief that the average provides a best guess as to the asset's "true" future value. Such simulations typically require statistical input in the form of the current price of the asset(s), the average price over a given time span, as well as the deviation from the average.

Let's suppose we are creating an application for valuing such instruments. As such, this application will need to download the statistical inputs via a Web service, and the details—such as URL and authentication information—for connecting to this service are stored in a configuration document. Suffice to say, the number of simulations to be performed for a given valuation request should also be flexible and, as such, will be specified via configuration.

Example 1: A basic configuration file

In this example, we create a basic configuration file, example1-config.xml, for our application, which holds the details for connecting to the Web service that provides the statistical inputs to the valuation process. This configuration file will also store the number of simulations to be performed for any valuation request. This file (as well as the configuration files for the other examples) is in the config directory of the downloadable archive associated with this tutorial. The contents of the configuration file are listed as follows:

 

<?xml version="1.0" encoding="UTF-8"?> <configuration>

<entry entryKey="market.data.service.url"> <value> https://www.some-exchange.com/marketdata </value> </entry>

<entry entryKey="market.data.service.uid"> <value>trading_app_dbo</value> </entry>

<entry entryKey="market.data.service.password"> <value>nopassword</value> </entry>

<entry entryKey="number.of.valuation.simulations"> <value>10000</value> </entry>

</configuration>

If we examine the file in more detail, notice that it starts with the root node <configuration>; this marks the beginning of an Obix configuration document. There are four <entry> nodes, each encapsulating a configuration entry. The first three hold the URL, user ID, and password for connecting to the inputs service; the final entry holds the number of simulations to be performed for each valuation request. Notice that each entry has a unique key, as specified by the entryKey attribute, and that the value in each entry is encapsulated by a <value> node.

Next, we create the skeleton of our valuation application, and, more importantly, we demonstrate how the configuration document is read at runtime. The class of interest is called Example1.java and can be found in the src folder of the downloadable archive associated with this tutorial. The class definition is as follows:

 

import org.obix.configuration.Configuration; import org.obix.configuration.ConfigurationAdapter; import org.obix.configuration.ConfigurationAdapterFactory;

public class Example1 { public static void main(String[] args) { ConfigurationAdapterFactory adapterFactory = ConfigurationAdapterFactory.newAdapterFactory();

ConfigurationAdapter adapter = adapterFactory.create(null);

adapter.adaptConfiguration(Configuration.getConfiguration(), "config/example1-config.xml"); printMarketDataInfo(); }

private static void printMarketDataInfo() { Configuration globalConfig = Configuration.getConfiguration();

System.out.println("Data Service URL :\t\t" + globalConfig.getValue("market.data.service.url"));

System.out.println("Data Service User-ID :\t\t" + globalConfig.getValue("market.data.service.uid"));

System.out.println("Data Service Password :\t\t" + globalConfig.getValue("market.data.service.password"));

System.out.println("Simulation Count :\t\t" + globalConfig.getValue("number.of.valuation.simulations")); } }

To run this and subsequent examples, you need to download the Obix Framework binaries to a location accessible via your classpath. Your classpath must reference the Obix library, obix-framework.jar, which can be found in the lib folder of the framework's root directory. You will also need the following third-party open source libraries: dom.jar, jaxen-full.jar, sax.jar, saxpath.jar, and xercesImpl.jar, which can be found in the lib/thirdParty folder of the framework's root directory.

Executing this class should produce the following result:

 Data Service URL :  https://www.some-exchange.com/marketdata
Data Service User-ID :          trading_app_dbo
Data Service Password :         nopassword
Simulation Count :              10000

To dissect this class, we start with the main method. The first line of this method creates an instance of the class org.obix.configuration.ConfigurationAdapterFactory, which is responsible for creating a configuration adapter (an instance of class org.obix.configuration.ConfigurationAdapter). The adapter, in turn, is responsible for actually reading a configuration document from a given location (specified as a file path or URL).

The following code extract reads the contents of our configuration file into the global/static configuration instance by invoking the adapter method adaptConfiguration(), and by passing a reference to the global instance—as obtained from the call Configuration.getConfiguration()—and the path to our configuration file config/example1-config.xml:

 adapter.adaptConfiguration(Configuration.getConfiguration(),
    "config/example1-config.xml");

Note that it is possible to create a new configuration instance to store our configuration data, rather than use the static (global) instance, but for the sake of simplicity (and brevity), we use the static instance for this example.

Next, we briefly examine the method printMarketDataInfo(), which simply reads the configuration entries (i.e., the <entry> XML nodes) and prints their values (i.e., their <value> child nodes). Notice that each entry's value is obtained by calling the method getValue (...) on the associated Configuration instance, passing in the name/key of the entry—as specified for the entry node's entryKey attribute. As an aside, note that an entry can have multiple values, which will be demonstrated later in this tutorial.

Example 2: Modularizing configuration data

Applications of this nature will typically generate a report detailing a request's results in some sort of format. Our hypothetical application is no different; it is capable of producing valuation reports in a variety of formats. In addition, the reporting formats used in a given application run are dictated by a configuration entry, and all generated reports are emailed to a list of recipients within our organization—where the recipients are also specified in the configuration set.

Logically, reporting is a distinct piece of functionality—when compared to valuation—even though both are related; so it would be quite reasonable to encapsulate our "reporting" configuration data. This not only provides a cleaner separation of the configuration data, but also makes it simpler for a novice to visualize the delineation of functionality within the application.

We encapsulate the reporting configuration for this example by creating a configuration module for reporting, which is a child of our root module. We modify the configuration file from the last example by appending the node shown below to its list of nodes; the resulting file is called example2-config.xml and can be found in the config directory of the source archive.

 

<?xml version="1.0" encoding="UTF-8"?> <configuration>

.................... .................... ................... <!--NEW NODE : a module holding reporting information--> <configuration-module moduleId="reporting.parameters"> <entry entryKey="reports.destination.email"> <value>risk_analysts@mybank.com</value> </entry>

<entry entryKey="report_formats"> <value>spreadsheet</value> <value>text-file</value> <value>pdf</value> </entry> </configuration-module>

</configuration>

Two things immediately stand out in this configuration file: the first, of course, is our module definition <configuration-module ....>, followed by the module's second entry node <entry entryKey="report_formats">. We begin with the module definition. An Obix configuration document can contain any number of submodules. Barring two elements—not discussed in this tutorial—modules support the same node set as the root module. In other words, modules have entries and can contain other modules; hence, modules can effectively be used to replicate a tree structure.

Recall that in the last example, I mentioned that a configuration entry can have multiple values. This functionality is demonstrated by the configuration entry for holding reporting formats, i.e., <entry entryKey="report_formats">. As you can see, this differs from other entries in that it has three values—specifying the three formats in which reports should be generated.

We now examine the Java code for reading the entries in our reporting configuration module. We modify the Java source for the previous example by adding the following method; the modified source file (class) is renamed Example2.java, and can be found in the src folder of the archive associated with this tutorial:

 

private static void printReportingConfig() { Configuration globalConfig = Configuration.getConfiguration();

Configuration reportingConig = globalConfig.getModule("reporting.parameters");

System.out.println("Reports Destination :\t\t" + reportingConig.getValue("reports.destination.email"));

System.out.println("Reporting Formats :\t\t" + reportingConig.getValues("report_formats")); }

On executing this class, it should produce the output:

 

Data Service URL : https://www.some-exchange.com/marketdata Data Service User-ID : trading_app_dbo Data Service Password : nopassword Simulation Count : 10000

Reporting Config Parameters= Reports Destination : risk_analysts@mybank.com Reporting Formats : [spreadsheet, text-file, pdf]

Upon examining the additional method in detail, we notice that it first obtains a reference to the global Configuration instance; then it proceeds to acquire a reference to the configuration module holding the reporting configuration information. The method achieves these tasks by invoking the method getModule(...) on the parent module, passing in the ID of the module to be received. Note that this syntax is generic in the sense that obtaining the child of any module—even if not the root module—is achieved by invoking getModule(...) on the given module.

1 2 Page 1