COMP 310

Dynamic Application Configuration

    Current Home  Java Resources  Eclipse Resources

There are many times in which one needs to change the values of various constants an application uses to configure its behavior.   A classic example is when one wants to run multiple instances of the same application at the same time on the same machine.    The operating system requires that I/O ports be unique between simultaneously running applications, so while the port being used by any given instance is a constant within that instance, the port must be different between instances.

We can gather all such constant values used by an application into a single "configuration" entity.  This configuration entity is essentially a mapping from a name to a value.  Applications commonly have configuration files that define these key-value mappings and the application reads this file at the beginning to get its configuration settings.

But even so, this does not tell us how to represent the configuration constants within the application itself.   Dictionaries can be used for this purpose but most dictionaries are mutable whereas the configuration data is immutable.

Representing Invariant Configuration Information

In Java, one way to represent a mapping from a name to a value is with a class.    Fields are mappings from names to values.   To enforce immutability, the fields can be made final.

Here's an example application configuration representation:

/**
 * Top-level configuration object. Defines the invariant of having a name.
 * All configuration types used in the application must be derived from this class.
 * An application or module should define a subclass that add additional final fields for 
 * configuration values needed by that application/module.
 * @author Stephen Wong
 *
 */
public abstract class AppConfig {
	/**
	 * The name associated with a particular configuration.  
	 */
	public final String name;
	
	/**
	 * Constructor for the configuration
	 * @param name The name associated with this specific configuration.
	 */
	public AppConfig(String name) {
		this.name = name;
	}
}

Here is a particular type of AppConfig that could be used in ChatApp-type applications:

/**
 * Class that holds the values needed for a particular configuration of a system instance.
 * The fields of this class are whatever are needed to configure different instances of the system.
 * The fields are all "final" because they should not ever be mutated since they are initial configuration information.
 * This particular configuration class is useful for ChatApp-type RMI applications.
 * @author Stephen Wong
 *
 */
private class PortConfig extends AppConfig {

	/**
	 * The numerical value associated with this configuration.
	 */
	public final int index;
	
	/**
	 * The port to use for all RMI stubs in this configuration.
	 */
	public final int stubPort;
	
	/**
	 * The port the class file server uses in this configuration.
	 */
	public final int classServerPort;
	
	/**
	 * Constructor for a configuration object
	 * @param name  The name associated with this configuration.  This is required by the AppConfig superclass.
	 * @param index The numerical value associated with this configuration.
	 * @param stubPort The port to use for all RMI stubs in this configuration.
	 * @param classServerPort The port the class file server uses in this configuration.
	 */
	public PortConfig(String name, int index, int stubPort, int classServerPort) {
		super(name);
		this.index = index;
		this.stubPort = stubPort;
		this.classServerPort = classServerPort;
	}
}

The various configuration values can easily be retrieved by simply referencing the immutable fields of the configuration object:

PortConfig currentConfig;

currentConfig.name
currentConfig.stubPort
currentConfig.classServerPort
//etc

The values for the configuration object can be read from a configuration file or from some fixed internal definitions if the possibilities are limited.

 

Starting a System with Configuration Information

In either case, something identifying the particular configuration to use must be passed to the model and view from the controller.   This identifier can be a "name" associated with a particular configuration indicating which internally defined configuration to use or which configuration file to read in.

The configuration name can be input via the application's GUI or passed in through the command line, e.g.

public class Controller {
	/**
	* 
	* @param appArgs The command line parameters that were separated by spaces 
	*/
	public static void main(final String[] appArgs){
	
		SwingUtilities.invokeLater(() -> {
			(new Controller(appArgs)).start();
		});
	}

	/**
	 * The configuration name used by this instance of the app
	 */
	private String appConfigName = "defaultConfig";
	
	/**
	 * Constructor for the controller
	 * @param appArgs appArgs[0] is the configuration name
	 */
	 public Controller(String[] appArgs){
		if(0<appArgs.length) {  // make sure that there is actually a configuration name supplied.
			appConfigName = appArgs[0];
		}	
		
		// Pass the configuration name along to the model and view
		// To enforce decoupling, the model and view should have separate configurations.
		model = new Model(appConfigName, ...) {...};
		view = new View(appConfigName, ...) {...};
	}
	//other code elided
}

Eclipse Launch (Run) Configurations:

In Eclipse, different launch (run) configurations can be set up to pass different configuration names to the application upon launch, thus easily creating different instances of the same applicaiton but with different configurations.

Simply make multiple copies of the same run configuration but changing the value specified in the Arguments tab to be the different configuration names.

 

Other Possibilities:

In the above example, the configuration name comes in via a command line parameter and is passed to the model and view respectively, delegating the loading of the configuration information to those modules respectively at construction time.   Here, the controller is completely decoupled from the mechanics of how the configuration information is initialized by either the model or view or any other components of the system.

There are some other possibilities however:

The model/view needs to be decoupled from the loading of the configuration information

In this situation, perhaps the configuration information is being loaded by a different part of the system before the model or view is instantiated.  

Since the user of the configuration is not the one loading it, it makes sense to pass the whole configuration object to the model, view or other component rather than merely the configuration's name.   Note that this method is independent of how the configuration information is loaded, be it from a file or from internal definitions as described below.

The configuration information is not determined until after the model or view (or whatever component needs it) has been instantiated:

A typical scenario here is that the view presents the user with a choice of configurations which, after the desired option is selected, the information about which needs to be passed to the model.    However, in this situation, the model has already been instantiated but not yet configured.    No command line parameter would be necessary here when starting the system.

In this situation, the configuration, either just its name or the entire configuration object (note that the view cannot instantiate the model's configuration object -- the controller would have to perform or have something perform the configuration information loading) to the model's start() method.   Effectively, the view is starting the model.  Once again, this technique is independent of how the configuration information is actually loaded.

 We can see that the above techniques are not entirely distinct from each other, so there are still other variations that might suit a particular applications needs more closely.   The critical design aspects center around maintaining maximum decoupling and to encapsulated operations whose internal details are not germane to the rest of the system.

 Launching multiple application instances from a "master launch":   The beauty of the above techniques combined with the MVC pattern is that they can easily be adapted to make multiple launches of the same application from a single "master launch" application.    In an MVC, all one needs is the instantiate the controller to create a new instance of the application and that controller merely needs to be instantiated with the desired configuration name to create an instance of an application with that configuration.

 

Utilizing Pre-defined Internal Configurations

Instead of reading configuration information from a file, different configurations can be stored inside the application itself in a mapping from configuration name to configuration objects.  This is only useful if there are only a few possible configurations of the application.   To load the configuration object is merely to use the desired name to retrieve the appropriate object from the configuration object dictionary.

The configuration map, AppConfigMap below, should not extend a builtin Map implementation such as HashMap because those classes allow mutation plus has many other methods that are probably unnecessary.  Better safety and encapsulation would be achieved by wrapping something like a HashMap with a simple, immutable class that only exposes a public  get() method plus a less accessible putConfig() method.

/**
 * An map of configuration objects (AppConfigMap.AppConfig derivatives) keyed by the 
 * name associated with the configuration. 
 * Use putConfig(config) to store configurations and get(name) to retrieve them. 
 * @author Stephen Wong
 *
 * @param <V> The type of AppConfig configuration being held by this mapping.
 */
public class AppConfigMap<V extends AppConfig> {

	/**
	 * Fully encapsulated configuration object storage to prevent outside mutation
	 */
	HashMap<String, V> configMap = new HashMap<String, V>(); 
	
	
	/**
	 * Constructor for the class that assumes that a subclass will load the configuration information.   
	 */
	public AppConfigMap() {
	}
	
	/**
	 * Alternate constructor for the class that allows an outside entity to supply
	 * the configuration objects.
	 * @param configs Vararg of configuration objects to load into the protected internal storage.
	 */
	@SafeVarargs
	public AppConfigMap(V... configs) {
		for(V config: configs) {
			putConfig(config);
		};
	}	
	
	/**
	 * Automatically store the given configuration object keyed to its name.
	 * Only this class or a subclass can load the configurations.
	 * @param config  The configuration object to store.
	 */
	protected void putConfig(V config) {
		configMap.put(config.name, config);
	}
	
	/**
	 * Retrieve the configuration object given its name.   
	 * @param configName The name of the desired configuration object
	 * @return The associated configuration object or null if it could not be found.
	 */
	 public V get(String configName) {
	 	return configMap.get(configName);
	 }
}
 

 

Here's the above AppConfigMap applied to the PortConfig configuration type, where there are three different configuration names, "primary", "secondary" and "ternary"  (There's nothing special about these names; use names that appropriately describe the different configurations of the system at hand.):

Using the above class, the configuration map can be initialized in a couple of ways.   Below, the configuration map is being shown as part of the model but as discussed above, it could be located in other parts of the system:

public class Model {

	
	// LOAD VIA A SUBCLASS:
	// This technique is useful when more control over the instantiation of the configuration objects is desired but without 
	// exposing them to the rest of the system before they are used.
	// An anonymous inner class is shown here to illustrate a subclass implementation but a regular named class could be used as well.
	
	/**
	 * Invariant configuration map that stores different configurations, keyed by the configuration's name
	 */
	final private AppConfigMap<PortConfig> configMap = new AppConfigMap<PortConfig>() {
	
		// The use of an anonymous inner class here with an initializer block enables one to load the map at the same time that it is being instantiated.
		// This code would be in the constructor of a regular named subclass.
		{
			// Initialize the configuration map with different configurations
			putConfig(new PortConfig("primary", 0, IRMI_Defs.STUB_PORT_SERVER, IRMI_Defs.CLASS_SERVER_PORT_SERVER));
			putConfig(new PortConfig("secondary", 1, IRMI_Defs.STUB_PORT_CLIENT, IRMI_Defs.CLASS_SERVER_PORT_CLIENT));
			putConfig(new PortConfig("ternary", 2, IRMI_Defs.STUB_PORT_EXTRA, IRMI_Defs.CLASS_SERVER_PORT_EXTRA));
		}
	};

	//  ------------ OR ------------

	// LOAD DIRECTLY VIA CONSTRUCTOR:
	// This is a simple technique which is useful when the configuration objects are coming from other parts of the system 
	// but has the drawback of having those objects exposed before they are used.
	
	/**
	 * Invariant configuration map that stores different configurations, keyed by the configuration's name
	 */
	final private AppConfigMap<PortConfig> configMap = new AppConfigMap<PortConfig>(
			new PortConfig("primary", 0, IRMI_Defs.STUB_PORT_SERVER, IRMI_Defs.CLASS_SERVER_PORT_SERVER),
			new PortConfig("secondary", 1, IRMI_Defs.STUB_PORT_CLIENT, IRMI_Defs.CLASS_SERVER_PORT_CLIENT),
			new PortConfig("ternary", 2, IRMI_Defs.STUB_PORT_EXTRA, IRMI_Defs.CLASS_SERVER_PORT_EXTRA));
	
	

	// other Model code elided
}

 

The desired configuration object is then simply retrieved from the configuration map using the supplied configuration name.   This process is being done in the model's constructor below but as pointed out in the above discussion, could be performed by other parts of the system:

public class Model {

	// Initialization of configMap elided.   See the above choices.
	
	/**
	 * The current app configuration. Declared as final to enforce its immutability. 
	 */
	final private PortConfig currentConfig;
	
	/**
	 * Constructor for the model.   
	 *
	 * @param configName  The name of the desired configuration  
	 */
	public Model(String configName, ...) {
		currentConfig = configMap.get(configName);	// Load the configuration object using its name
		
		if(null == currentConfig) {	// First check to make sure that a valid configuration was found.
			throw new IllegalStateException("Invalid configuration selected! configName = " + configName);  // Cannot continue if the desired configuration is invalid.
		}
		else {
			// Normal model operations elided.
		}
	}
	
	// other code elided
	
}

As discussed above, the configuration map could be located in another part of the system, outside of the model.   In those scenarios, the selected configuration object itself would be passed to the model, not just its name.

 

 

 

 

 


© 2017 by Stephen Wong