Author: Michael Grove
For: MINDSWAP
cHECK OUT THE NEW HOMEPAGE This is the OWL Class Loader, a tool that will dynamically load and create Java objects on the fly using the Java Reflect package. How? Let's take a look at an example. To create a Vector in Java I would normally do something like this:
Vector myVector = new Vector();
At this point I have a nice little Vector to store things in. So how does this work with the Class Loader? The answer is, almost the exact same way. However, instead of using the Java language directly, we're using the Web Ontology Language (OWL) to tell us how to create a Vector, then we create it based on that information. Let's take a look at how it's done:
<cl:Object rdf:ID="myVector">
<cl:hasClass>java.util.Vector</cl:hasClass>
<cl:hasConstructor rdf:resource="http://www.mindswap.org/~mhgrove/ClassLoader/classLoaderOnt.owl#DefaultConstructor" />
</cl:Object>
This snippet of OWL from one of my testing files will do the EXACT same thing the previous line of Java code did. It will create an object of type java.util.Vector, as specified by the hasClass field, and will use the default constructor as shown by the hasConstructor field. This OWL essentially translates into the Java code shown earlier. The only difference is it's not stored in a variable called myVector. But it's stored in something pretty close, we'll get to that later.
So lets take a more complicated example. what if I want to pass arguments to my constructor? For example, if I want to create a Hashtable with a certain load factor and initial capacity, I would do so like this:
// variables initialized...
int capacity = 20;
float loadFactor = 1.2;
// some code...
Hashtable myHash = new Hashtable(capacity,loadFactor);
So this is a little more complex. I need to specify arguments sent to the constructor, and not only specify arguments, they need a certain type and value. No problem! Let's see how the Class Loader handles this situation:
<cl:Object rdf:ID="myHash">
<cl:hasClass>java.util.Hashtable</cl:hasClass>
<cl:hasConstructor>
<cl:Constructor>
<cl:parameterList rdf:parseType="daml:collection">
<cl:Parameter>
<cl:hasClass>int</cl:hasClass>
<cl:withValue>20</cl:withValue>
</cl:Parameter>
<cl:Parameter>
<cl:hasClass>float</cl:hasClass>
<cl:withValue>1.2</cl:withValue>
</cl:Parameter>
</cl:parameterList>
</cl:Constructor>
</cl:hasConstructor>
</cl:Object>
First thing you notice is that this is a much more complex piece of OWL. We still specify the type of this object using the hasClass term, but the way we describe the constructor has changed. Since we are no longer using the default constructor for this object, we need to specify the parameters the constructor takes as well as the type and value of each of these parameters. So in the Java example, the constructor took two parameters, one int for the initial capacity, and one float for the load factor. If you look carefully at the constructor specification, you'll see that two parameters are specified, one of type int, and one of type float. And each parameter has the same value as in the Java example. The Parameter tags are very straightforward. Again you'll see the hasClass tag used to indicate the type of the parameter just as it specifies the type of the object being created. And the withValue tag which specifies the value for that parameter. This snippet will yield a Hashtable with initial capacity of 20, and a load factor of 1.2 just like the Java code did. It's as easy at that! The most important thing to remember when specifying a constructor as such is that order matters when giving the list of parameters, so be careful!
Well, now you might be thinking, "That's a pretty cool system. But I need more fine grain control over my objects. My class Foo takes a Vector of full of items. Right now you've shown that you can only create an object. I NEED my object to have stuff in it!" No problem! You can invoke methods on your objects just like you would in Java. For example, perhaps I'm doing some parsing, and I need to process a list of tokenized objects, the Class Loader can create you list of StringTokenizers for you so you can get down to processing! Let's see how we'd do it in Java first:
// this is our vector for holding the tokenizers
Vector myVector = new Vector();
// lets create a sample tokenizer for our list
// in reality there'd be more than just one...
String toParse = "hello how are you today?";
String delims = "\n ,.:?";
boolean returnDelims = false;
StringTokenizer st = new StringTokenizer(toParse,delims,returnDelims);
myVector.addElement(st);
So three things happened in that code. One, we created a Vector. Two, we created a StringTokenizer. Three, we invoked a method on the Vector passing the StringTokenizer as an argument. So the first two items should not come as any surprise to you at this point, we've already seen how we can create objects. So let's just briefly look at that code (along with the method execution code, which we'll discuss in a minute):
<cl:Object rdf:ID="st">
<cl:hasClass>java.util.StringTokenizer</cl:hasClass>
<cl:hasConstructor>
<cl:Constructor>
<cl:parameterList rdf:parseType="daml:collection">
<cl:Parameter>
<cl:hasClass>java.lang.String</cl:hasClass>
<cl:withValue>hello how are you today?</cl:withValue>
</cl:Parameter>
<cl:Parameter>
<cl:hasClass>java.lang.String</cl:hasClass>
<cl:withValue>\n ,.:?</cl:withValue>
</cl:Parameter>
<cl:Parameter>
<cl:hasClass>boolean</cl:hasClass>
<cl:withValue>false</cl:withValue>
</cl:Parameter>
</cl:parameterList>
</cl:Constructor>
</cl:hasConstructor>
</cl:Object>
<cl:Object rdf:ID="myVector">
<cl:hasClass>java.util.Vector</cl:hasClass>
<cl:hasConstructor rdf:resource="http://www.mindswap.org/~mhgrove/ClassLoader/classLoaderOnt.owl#DefaultConstructor" />
<cl:methodsToExecute rdf:parseType="daml:collection">
<cl:Method>
<cl:methodName>addElement</cl:methodName>
<cl:parameterList rdf:parseType="daml:collection">
<cl:Parameter>
<cl:hasClass>java.lang.Object</cl:hasClass>
<cl:withValue rdf:resource="#st"/>
</cl:Parameter>
</cl:parameterList>
</cl:Method>
</cl:methodsToExecute>
</cl:Object>
So first things first. You'll see that I create the StringTokenizer just like I would in Java (remember order doesn't matter in OWL files). Next, when you look at the tag to create the Vector, at first you'll notice it looks very familiar with the hasClass tag and the default constructor specified, just like in our first example. This time there's a little something extra added in. The methodsToExecute tag specifies a list of methods to invoke on this object. Each method takes a methodName which you'll notice is the name of the method we'd like to execute, and the list of parameters that method takes. Parameters are specified for the Methods the same way they are for Constructors (after all a Constructor IS a method!). So when this is run through the Class Loader, both the Vector and StringTokenizer objects are created, then the methods are executed on the Vector. You end up with the same results as you did with the Java code.
Lastly, how do you got your objects back once you create them? Currently, a Hashtable is returned from the loader that contains all the objects created. Each object is keyed on its rdf:ID attribute. So in the case of the myVector example, the id will be something like "http://example.org/myConfigFile.owl#myVector." You can enumerate through the hashtable to figure out which object is which and use them appropriately. Alternatively, you can do what I did, write an ontology that extends the basic class loader ontology. The framework is designed so you don't have to create objects from an entire file, all you need is the snippet of OWL that defines them to create them. So you could have a broader ontology that defined in a larger scope what you are doing, and a piece of it would detail how to create the associated object. This way as you read in the file, you can create the objects as you come to them, and you already know the context in which they are being used and can assign/process them approrpriately.
By now you're probably thinking, what good is any of this. Clearly it's more difficult to write "code" in OWL than in Java, so why bother? Well my intent isn't for someone to "write code" using this system. I initially designed it out of laziness and a shortage of time. I have a full time job, but I'm also a member of the MINDSWAP group and I have a couple projects that I work on for them in my spare time. But I work alot, so I don't have a lot of spare time, and certainly not a lot of spare time I really want to spend coding, especially since I code all day. So I needed to maximize the time I have.
I didn't know I needed a system like this until my main project RIC began to grow to a point where I could no any longer keep track of it. The code swelled to 200+ classes, and over what I estimate to be around 15,000 lines of code. I was going crazy! Not to mention there were clones or spin-offs like Agenda-RIC that were being created, but were separate programs altogether. I had to sift through SO much stuff just to change how the disabling worked on a certain button on a certain window I wasted a lot of time. And that one change I made, did not apply to all the programs that used what I changed...they all had their own copy of the code. So I thought, "Maybe I should divide all this up into smaller modules. I can reuse them across programs. Make one change its changed everywhere. No need to recompile old stuff." The usual stuff you pick up in design classes in school or trial and error in real life. Then I figured if I was going to all the trouble of making each part it's own module, I should have some efficient and sane way to load and assemble each piece to create the program as a whole. I also had noticed, that other's work, and some of my own unrelated work, would fit nicely into RIC as extensions or add-ons. ConvertToRDF is an example for one such extension. So great, now I have a big program, all these little modules that make up the program, and it's all hard coded. What if I need to build RIC with the ConvertToRDF module? Or what if a colleague is showing RIC to one group of people in the morning, and Agenda-RIC to another group in the afternoon? This would require changes in the code and recompilation for that to work! Well that doesn't help any. What if the program built itself depending on some sort of configuration setup? What if I could dynamically create the objects needed for running the program that time around, then later if I need to change the behavior, I can just turn parts off, or switch old ones out for new ones? What and I don't have to recompile every time? That's wonderful! And my OWL Config system for RIC was built.
So now the RIC core sits on my desktop, and is rarely ever touched. I need a search feature? Ok no problem. I write the code to do the search feature, put the description in RIC's config file so it can create my Search class, and voila, I have a search feature in RIC. The search module, once created, knows how to add itself to the program (it's built into the RIC framework, which the search module was built on). Sound too good to be true? It's not! I did it yesterday ;)
So here's some files for you to look at:
The class loader ontology
A demo file using one of the above examples.
RIC's Configuration ontology (extends and leverages the class loader ontology)
A sample configuration file from RIC
Check out RIC which uses this software.
Download the Jar file.
Read the javadocs.
Also, I'd really love to hear feedback from anyone who reads this page and is interested in what it discusses. Like I said before, I developed it as a tool for myself to make my life developing code much easier, but if someone thinks this could be of broad use, I'd love to hear your ideas. Or if you think I did something kind of backwards and think it would greatly improve the system if I changed it, I'm happy to hear that as well.
How the System Works in RIC
I would like to briefly discuss in a little more detail how this class loading system works within my program RIC. It will greatly help if you are familiar with RIC and have looked at the RIC config files provided above. First thing you'll notice when looking at RIC's config file is that it doesn't look anything like the previous examples. As I mentioned, I built a small layer on top of the class loading scheme which leverages that power of dynamically creating objects with the ability OWL gives me to provide meaning to something. So my RIC config file simply details for each basic component in the system, what class to load (and how). Then the class loader is used to create and load that particular class. Once created, it's used in the correct manner because RIC's config reader knows that the hasWorkspace property of its configuration will give it the object to use as it's workspace. This is done for each of the core components, RIC is told which component it is, and what object to load and use for that component.
The same is done for any extensions to the program. The config reader for RIC reads each of these extensions. For each one that is active, it creates an object of the specified type, and registers that object with the main program, RIC (how that happens is more related to the design of the RIC framework than this particular discussion). In this way, I can load (or not load) any number of additional modules to increase, change, or limit functionality depending on my current needs. So now to "compile" the Agenda-RIC program from the RIC code, all I need to do is put the correct OWL in the config file and make sure that its marked active. No recompiling is needed. Modules can be upgraded without changing code, just tell the program what the new module is to load, and voila, the program uses the new and improved part, rather than the old one you replaced.
