Java introspection: Easier reading and writing of Notes documents

This article tells how to create a new, reusable Java class called NotesDocumentObject that simplifies reading and writing with the back-end classes for Java by using introspection, a Java program's ability to examine itself.

Share:

Pete Lyons, Software Engineer, Iris Associates

Pete Lyons has been with Iris for just over one year, where he has been working exclusively on the Notes development environment. Prior to Iris, he spent 13 years as the UI and programmability architect for Alpha Software's family of database products.



02 February 1998

Introduction

Luckily, there is an easier way. Using some cool Java features and a little object-oriented programming, you can develop a class that does the dirty work for you.

One of the differentiating features of Java over LotusScript or C++ is that while it's running, a Java program can examine itself. A Java program can ask one of its objects what class it is, and then inspect the makeup of the class. It can also find out the names and data types of all the properties and methods that make up the class. This ability is referred to as introspection.

This article will show you how to use introspection to build a new base class, called NotesDocumentObject, that can simplify the reading and writing of Notes documents. We'll first look at a sample Notes document, and how the Java code for that document works without introspection. Then, we'll walk through how to create the new, reusable NotesDocumentObject class, which uses introspection to examine itself and figure out automatically what items to read and write. This new class will have the following public interface:

public class NotesDocumentObject {
public void read(Document doc);
public void write(Document doc);	
}

Reading and writing from Notes -- without introspection

Before we get into how you use introspection, let's first look at how you read and write from Notes without using introspection. For example, the following screen shows a simple Notes document composed of six items: Type, From, Department, CostCenter, Items and Processed.

Figure 1. Requisition form
Requisition form

The Java class for this type of document would then look something like the following.

public class OldStyleSupplyRequest {
String type;
String from;
String department;
String costCenter;
Vector items;
String processed;

public void read(Document doc) throws Notes Exception {
    type = doc.getItemValueString("Type");
    from = doc.getItemValueString("From");
    department = doc.getItemValueString("Department");
    costCenter = doc.getItemValueString("CostCenter");
    items = doc.getItemValue("Items");
    processed = doc.getItemValueString("Processed");
}

public void write(Document doc) throws Notes Exception {
    doc.replaceItemValue("Type", type);
    doc.replaceItemValue("From", from);
    doc.replacetItemValue("Department", department);
    doc.replaceItemValue("CostCenter", costCenter);
    doc.replaceItemValue("Items", items);
    doc.replaceItemValue("Processed", processed);
}
}

This code isn't hard to write, but it could become difficult to maintain, especially if there were a lot of items. For example, if you want to change a name, you must manually change it within the code in three places: in the declaration, and in the read and write methods.

If you use introspection, you can instead create a reusable class to derive the items, and then automatically read and write them.


How introspection works

Before we begin to use introspection to build our NotesDocumentObject class, let's look at how the introspection code works. The key Java classes involved with introspection are:

  • java.lang.Object
  • java.lang.Class
  • java.lang.reflect.Field

Since every Java class derives from java.lang.Object (whether explicitly declared or not), you can always call java.lang.Object.getClass() to find information on the class of the object. This information is returned in the form of an object of type java.lang.Class. With a java.lang.Class object, you can call java.lang.Class.getDeclaredFields() to get an array of all the properties declared in the class. The array of property information contains elements of the type java.lang.reflect.Field.

(In Java, properties are referred to as "fields," but don't get confused, we're not talking about the fields in a Notes database here -- we are talking about

the properties of a class.) Once you have the array of java.lang.reflect.Field objects, you can loop through and determine the corresponding property's name, data type and value. The following table shows the methods from java.lang.reflect.Field that we will use to create our NotesDocumentObject class:

Figure 2. Java.lang.reflect.Field methods
Java.lang.reflect.Field methods

Let's go back to our sample Notes document. Once we develop the NotesDocumentObject class, we can use the following code to read and write the sample data.

public class SupplyRequest extends NotesDocumentObject { 
String type;
String from;
String department;
String costCenter;
String[] items;
String processed;
}

The read and write methods declared in our parent class NotesDocumentObject will automatically scan through the SupplyRequest class and figure out what Notes items to access. That's certainly a lot simpler than needing to hard-code the solution each time!


Mapping data types between Java and Notes

We have one more thing to look at before we actually create the NotesDocumentObject class. As our new NotesDocumentObject class is scanning through its sub-class, it needs to know how to read and write the various data types it encounters. We could do a simple implementation that only handles a couple of data types, however, we'd prefer for this class to be an extensible tool. We will do this by creating a dynamic table of classes that contains handlers for all the different data types NotesDocumentObject can process.

We'll base the data conversion handlers on an abstract base class that defines how to translate a Java data type in and out of Notes. The class we will use is called ClassMap. As shown below, ClassMap has a constructor with a parameter for the specific data type it maps, a method called isMapFor that is used to check what class the ClassMap processes, and a read and write method for getting the data in and out of Notes.

public abstract class ClassMap {
    Class dataType;

    public ClassMap(Class dataType) {
        this.dataType = dataType;
    }
    public boolean isMapFor(Class dataType) {
        return this.dataType.equals(dataType);
    }
    public abstract Object read(Document doc, String name) 
        throws Exception ;
    public abstract Item write(Document doc, String name, Object o) 
        throws Exception ;
}

This ClassMap class is abstract because we don't define the implementations of the read and write methods. There will be a derivative of ClassMap for each data type we decide to map. Each ClassMap derivative then defines its own read and write methods for moving the data type that it represents.


Setting up mappings for NotesDocumentObject

To keep things simple, we'll set up our NotesDocumentObject class with mappings for the two most common data types: String and String[ ]. You should then be able to customize the class to handle additional mappings later.

Let's look at the simplest mapping first -- a java.lang.String class. To handle the String data type, we created the following StringClassMap. If you look at the read and write methods for StringClassMap, you can see how simple it is -- just plain getItemValueString and replaceItemValue calls. Notice that in the constructor, we pass the super class ClassMap the Class object for the class we represent, which, in this case, is String.class.

public class StringClassMap extends ClassMap {
    public StringClassMap() {
        super(String.class);
    }
    public Object read(Document doc, String name) throws Exception {
        if (doc.hasItem(name))
            return doc.getItemValueString(name);
        else
        return null;
    }
public Item write(Document doc, String name, Object o) throws Exception {
        return doc.replaceItemValue(name, o);
    }
}

We also created a mapping for the java.lang.String array. For this mapping, we created the following StringArrayClassMap. Although the Notes back-end classes don't support reading and writing directly to or from a String array, they do allow us to access a mult-value field. So, we create a java.util.Vector class that contains all of the values. Then, we just need to convert the java.util.Vector that contains the strings into a String array. We do this by creating a String array and then copying all the values from the Vector into the array.

public class StringArrayClassMap extends ClassMap {

public StringArrayClassMap() {
    super(String[].class);
}

public Object read(Document doc, String name) throws Exception {
    if (doc.hasItem(name)) {
        Vector v = doc.getItemValue(name);
        if (v != null) {
            int x;
            int size = v.size();
        String[] sa = new String[size];
            for (x = 0; x < size; x++) {
            sa[x] = (String) v.elementAt(x);
        }
        return sa;
        }
    }
    return null;
}

public Item write(Document doc, String name, Object o) throws Exception {
    String[] sa = (String[]) o;
    int x;
    int size = sa.length;
    Vector v = new Vector();

    for (x = 0; x < size; x++) 
        v.addElement(sa[x]);
    return doc.replaceItemValue(name, v);
    }
}

Although you now have two mappings, you should create more to utilize the full power of Notes. Some other mappings you may want to create include the following.

Figure 3. Java-to-Notes mappings
Java-to-Notes mappings

I know that creating the mapping for all the data types you want to support is not fun, but the good news is you only ever need to do this once.


Pulling it together: Writing the reusable NotesDocumentObject class

Now that you have the basic class mapping code written, all that's left to write is the NotesDocumentObject class. There are quite a few methods here, so let's take them one at a time.


Creating the mapping table

We created the mapping classes already, but we still haven't done anything with them. What we need to do is actually organize them into a table. Check out the following code snippet.

public class NotesDocumentObject {

static Vector typeMap = new Vector;

static { 
    typeMap.addElement (new StringClassMap());
    typeMap.addElement (new StringArrayClassMap());
    // Add more here
}

...

We create a Vector and load it up with instances of the mapping classes we defined earlier. This code only has the mappings for String and String[] data types, but you can write more and plug them into the same place. Notice that you only need one table for all instances of the NotesDocumentObject class, so all the table-related methods are declared static.


Finding the correct mapping

When it comes time to read or write a property from a class, we need to look up the correct ClassMap for doing the transaction. The following findDataType() method searches the static Vector typeMap (from above) for a mapping that matches the requested data type. If the method finds a match, it returns the ClassMap object; otherwise, it returns null. A null signals that this data type cannot be read or written.

static ClassMap findDataType(Class dataType) {
    int x;
    int count = typeMap.size();
    ClassMap classMap;

    for (x = 0; x < count; x++) {
        classMap = (ClassMap) typeMap.elementAt(x);
    if (classMap.isMapFor(dataType))
        return classMap; 
    }
    return null;
}

Reading items

Now for some fun. This is where we pull together all the work we have done so far. Let's start by looking at the code that does the read.

public void read(Document doc) throws Exception {
    int x;
    ClassMap classMap;
    Field[] fields = getClass().getDeclaredFields();

    for (x = 0; x < fields.length; x++) {
    classMap = findDataType(fields[x].getType());
    if (classMap != null)
    fields[x].set(this, classMap.read(doc, fields[x].getName()));
    }
}

The read method does the following:

  • Figures out the current class.
  • Gets the list of properties from the class (an array of java.lang.reflect.Field classes).
  • Loops through and reads all the properties from the specified document.

We can figure out the current class and get its list of properties with the following line of code.

Field[] fields = getClass().getDeclaredFields();

We can safely chain the two calls together, because getClass() always returns a valid class.

The loop that does the reading is a little more complicated, but it turns out to be only a few lines of code too. The loop will need to:

  • Find the class map for the data type of the current property (java.lang.reflect.Field).
  • Read the value from the item in the Notes document that has the same name as the property.
  • Write the value into the property of the Java class.

To find the class map, we call the findDataType() method we wrote earlier. We pass it the data type of the property we are trying to read. If the method finds a match, it returns the ClassMap object; otherwise, it returns null. We can use the ClassMap to read the item from the document. We read and set the value with the following line of code.

fields[x].set(this, classMap.read(doc, fields[x].getName()));

We call classMap.read() to read the item with the same name as the name specified by field[x].getName(), then we store the result in the property.


Writing items

Writing the class is just the reverse of reading it. The code looks almost exactly the same.

public void write(Document doc) throws Exception {
    int x;
    Field[] fields = getClass().getDeclaredFields();
    ClassMap classMap;

    for (x = 0; x < fields.length; x++) {
        classMap = findDataType(fields[x].getType());
        if (classMap != null)
        classMap.write(doc, fields[x].getName(), fields[x].get(this));
        }
    doc.save(true, false);
}

The first difference is that we call classMap.write() and fields[x].get() rather than classMap.read() and fields[x].set(). The second difference is that once we've written all the items, we can call doc.save() to commit the changes.


Putting it to use

Now that the classes are all written, let's make a test program so we can see the code in action. This program will first write out the supply request information we used in our initial example, and then read it back in. This is not an exhaustive test, but it will show the class at work.

import lotus.notes.*;

public class IntrospectionTest extends NotesThread {

    public static void main(String argv[]) {
        IntrospectionTest introspectionTest = new IntospectionTest();
        introspectionTest.start();
    }

public void runNotes() {
    try {
             Session session = Session.newInstance();
        Database db = session.getDatabase(null, "supply.nsf");
             Document doc = db.createDocument();

                // Sample write
                SupplyRequest supplyRequest = new SupplyRequest();

                supplyRequest.type = "SupplyRequest";
                supplyRequest.from = "Gail Weathers/Acme";
                supplyRequest.department = "Sales";
                supplyRequest.costCenter = "GS10";
            String [] temp = {"1020x2: Box of 10 red felt-tip pens", 
                "3302x4: 11x8 College-ruled notebook",
                "0014x10: Box of 20 blue ball-point pens",
                "0792x1: Model 7 stapler"};
            supplyRequest.items = temp;
                supplyRequest.write(doc);

                // Sample read
SupplyRequest testSupplyRequest = new SupplyRequest();
              testSupplyRequest.read(doc);

            } catch (Exception e) {
                e.printStackTrace();
            }
    }
}

Conclusion

The NotesDocumentObject is a valuable tool. Once you write some more ClassMaps, you may even find it indispensable. Remember, Java is a rich language that offers a host of new and exciting features that can make your programming life easier. I hope this article has shed a little light on the power of introspection, and opened your mind to the possibilities Java presents.

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into IBM collaboration and social software on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Lotus
ArticleID=12643
ArticleTitle=Java introspection: Easier reading and writing of Notes documents
publish-date=02021998