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 $