CSC207 Software Design
Lectures
Configuration

Recapitulation

MultiFilter runs text through multiple user-specified filters

Like Unix command-line pipes

Each filter class is derived from AbstractFilter

So that we know how to call it

Example:

java MultiFilter -e "repeat 3" -e "sample 2" -i t3.txt -i t4.txt

Reads from t3.txt, then t4.txt

Pass those lines of text to the "repeat" filter

"3" tells "repeat" how many times to repeat the line

Output of "repeat" is passed to "sample"

"2" tells "sample" to take every second line

Result written to standard output

The Problem

MultiFilter.createFilterList() is shown below

Only way to add a new filter is to modify this method, then recompile

Feasible for a program this small...

...but not for larger frameworks like web servers

protected List createFilterList(List filterConfigs) throws FilterException {
    List filterList = new LinkedList();
    Iterator ic = filterConfigs.iterator();
    while (ic.hasNext()) {
        String line = (String)ic.next();
        String[] fields = line.split(" ", 2);
        String filterName = fields[0];
        String filterArgs = (fields.length > 1) ? fields[1] : "";
        AbstractFilter filter = null;
        if (filterName.equals("echo")) {
            filter = new FilterEcho();
        }
        else if (filterName.equals("repeat")) {
            filter = new FilterRepeat();
        }
        else if (filterName.equals("sample")) {
            filter = new FilterSample();
        }
        else if (filterName.equals("sort")) {
            filter = new FilterSort();
        }
        if (filter == null) {
            throw new FilterException("Unrecognized filter '" + filterName + "'");
        }
        filter.configure(filterArgs);
        filterList.add(filter);
    }
    return filterList;
}

Solution:

Put class names in XML configuration file

Use those names to look up classes at runtime

Pass the XML configuration to the object's constructor when creating it

Let the object configure itself

Users can add whatever new classes they want

Without modifying the original program

Step 1: Design Configuration File Syntax

Root element is MultiFilter

Contains zero or more Filter elements

Each has a "className" attribute

Each Filter contains zero or more Param elements

Each has "name" and "value" attributes

Use attributes because parameters can't be nested

Do not specify input, output, debugging level, etc.

Those will change from run to run far more often that filter configuration

<?xml version="1.0" ?>
<!-- config3.xml -->
<MultiFilter>
    <Filter className="FilterRepeat">
        <Param name="times" value="3"/>
    </Filter>
    <Filter className="FilterSample">
        <Param name="skip" value="2"/>
    </Filter>
</MultiFilter>

Step 2: Load Classes

Java programs can load classes dynamically while they are running

"Just" reading a file, and creating a data structure in memory

If the class you want is on your class path, Java will load it automatically when you need it

E.g., when you call Class.forName("YourClass")

If you want to load classes that aren't on your class path:

Make sure you understand the security implications

Extend the ClassLoader class

public class SimpleClassLoader extends ClassLoader {

    // Test the class loader
    public static void main(String[] args) {
	ClassLoader loader = new SimpleClassLoader();
	for (int ia=0; ia<args.length; ++ia) {
	    try {
		Class c = loader.loadClass(args[ia]);
		System.out.println("Loaded " + c.getName());
	    }
	    catch (ClassNotFoundException e) {
		System.err.println(e);
	    }
	}
    }

    //------------------------------------------------------------

    // Create the class loader
    public SimpleClassLoader() {
    }

    // Load a specific class
    public Class loadClass(String className) throws ClassNotFoundException {
	Class result;
	result = super.findSystemClass(className);
	Class c = loadClass(className, true);
	resolveClass(c);
	return result;
    }
}

Note: a real class loader is more complex

Read bytes from a databse or web connection

Use those to create the class

No, this won't be on the exam...

Step 3: Create Instances of Classes

Loop over the Fitler elements of the configuration XML in order

Use Class.forName() to get the specified class

Use Class.newInstance() to create an instance of the class

Note: Class.newInstance() calls a class's zero-argument constructor

So each filter class has to provide one

No way to enforce this in the language

I.e., no way for a parent class to say, "All my children must have zero-argument constructors"

Note also: this implies two-step object construction

Create with Class.newInstance(), then configure with filter.configure(args)

Instead of passing configuration arguments to constructor with new Filter(args)

Two-step construction is more fragile than constructing objects in a single step...

...but in this case, there's no alternative

protected AbstractFilter createFilter(Element config) throws FilterException {
    assert config != null;
    assert config.getName().equals(ELT_FILTER);

    String className = config.getAttributeValue(ATTR_CLASSNAME);
    if (className == null) {
        throw new FilterException(ELT_FILTER + " element missing " + ATTR_CLASSNAME + " attribute");
    }
    AbstractFilter filter = null;
    try {
        Class filterClass = Class.forName(className);
        filter = (AbstractFilter)filterClass.newInstance();
        filter.configure(config);
    }
    catch (ClassNotFoundException e) {
        throw new FilterException(e);
    }
    catch (IllegalAccessException e) {
        throw new FilterException(e);
    }
    catch (InstantiationException e) {
        throw new FilterException(e);
    }
    return filter;
}

Step 4: Configure Individual Objects

Old method was Filter.configure(String args)

New method is Filter.configure(Element args)

Always pass in the Filter element, and let the filter object get the values it needs

Could get the list of Param elements, and pass them in instead

Individual filters are going to:

Search the Param elements to find parameters with particular names

Parse the values in those Param elements to get integers, strings, etc.

So add helper methods to the AbstractFilter parent class to do this

int getIntParam(Element config, String paramName)

String getStringParam(Element config, String paramName)

Etc.

class FilterRepeat extends AbstractFilter {
    public void configure(Element config) throws FilterException {
        assert config != null;
        fRepetition = getIntParam(config, "times");
        if (fRepetition <= 0) {
            throw new FilterException("Cannot repeat negative number of times");
        }
    }
}

Those Helper Methods

abstract class AbstractFilter {

    protected int getIntParam(Element config, String name) throws FilterException {
        assert config != null;
        String attr = getStringParam(config, name);
        int val;
        try {
            val = Integer.parseInt(attr);
        }
        catch (NumberFormatException e) {
            throw new FilterException("Cannot parse integer for '" + name +
                                      "' from '" + attr + "'");
        }
        return val;
    }

    protected String getStringParam(Element config, String name) throws FilterException {
        assert config != null;
        Element param = null;
        Iterator ic = config.getChildren().iterator();
        while (ic.hasNext() && (param == null)) {
            Element p = (Element)ic.next();
            if (p.getAttributeValue(MultiFilter.ATTR_NAME) == null) {
                throw new FilterException("Configuration parameter has no '" +
                                          MultiFilter.ATTR_NAME + "' attribute");
            }
            if (p.getAttributeValue(MultiFilter.ATTR_NAME).equals(name)) {
                param = p;
            }
        }
        if (param == null) {
            throw new FilterException("Cannot find attribute '" + name + "'");
        }
        String result = param.getAttributeValue(MultiFilter.ATTR_VALUE);
        if (result == null) {
            throw new FilterException("Configuration parameter has no '" +
                                      MultiFilter.ATTR_VALUE + "' attribute");
        }
        return param.getAttributeValue(MultiFilter.ATTR_VALUE);
    }
}

Summary

New MultiFilter is slightly more complex than the original

The big switch statement is gone

We're parsing XML instead of parsing strings

New MultiFilter is much easier to extend

Anyone who wants to add a new filter can do it without modifying the main program

Just as you can add new command-line tools in Unix

Don't need to use XML for this

Could invent our own text format for filters configuration...

...then write and debug a parser...

...and document it...

...and then deal with complaints from people who want to use other tools to parse or create filter configuration files

OK, it's unlikely anyone actually would...

...but they do it all the time for web server and database configuration files

Do need to use reflection to do this

Can do it in C/C++ as well (somewhat more complicated, but doable)

And in Python (the import keyword is just a call into a Python library)

And in---but you get the picture

The bigger a program is, the more effort you have to put into making it modifiable and extensible

This is the best solution we've come up with so far


$Id: config.html,v 1.1.1.1 2004/01/04 05:02:31 reid Exp $