Writing Java Agents
To use Domino Designer to create a Java agent, do the following:
- Open the database that will contain the agent.
- Select Create, Design, Agent.
- Name your agent something like Filter Junk Mail or Reschedule Appointments.
- Choose whether it's a shared agentthat is, one that other people will be able to useor a private agent that only you can run.
- Select the agent trigger.
- Determine which documents you want your agent to process or choose Run Once.
- Select Java from the Run list box in the Programmer's pane.
You will then see something like Figure 18.1.
Figure 18.1. Domino Designer starts off your agent with a simple code template.
As you can see, Domino Designer starts you off with some sample Java source code. Here's a slightly reformatted version of this simple template:
import lotus.domino.*; public class JavaAgent extends AgentBase { public void NotesMain() { try { Session session = getSession(); AgentContext agentContext = session.getAgentContext(); // (Your code goes here) } catch(Exception e) { e.printStackTrace(); } } }
This is a complete class definition that declares an agent named JavaAgent. Although it doesn't really do anything yet, all the elements needed for a Java agent are here. I'll discuss each in turn .
Packages
The first line is an import statement that tells the Java compiler that this class makes use of the lotus.domino package. You will want to put this statement in every source file that uses classes from the Notes API, such as Session and AgentContext .
A package is a named group of related classes. Packages use a hierarchical naming system to prevent name clashes with other code. The widespread convention is to start package names with your domain name reversed , as in com.libertastechnologies or com.ibm. As you can see, Lotus opted to leave off the com. Each level in the hierarchy is then separated by periods, so you can have lotus.domino (the Notes Java API package) or com.libertastechnologies.domino. utils . Package names are essentially paths to your source and class files. Each name in the hierarchy maps to a directory or subdirectory. To see this for yourself, use a tool such as WinZip to open the NOTES.JAR file and look at the class files and directories. JAR files are a handy way to collect class files together into one big compressed file. NOTES.JAR contains most of the Java files used by Notes and Domino, including some by IBM and others by a company called Acme.
Most of the lotus.domino classes are duplicated in lotus.notes, which was the package name of the Notes Java API in previous releases. The old classes are still around for backward compatibility. Use lotus.notes if you need your code to work with older installations. Use lotus.domino if you want to use the new features of R5.
Just as files can be specified with a pathname, classes can be specified with their fully qualified package name, as in lotus.domino.Session. If you want, you could prefix all the class names in your code with their package name, but this quickly becomes cumbersome. import statements are easier. These tell the compiler which packages to search in order to find the classes in your code. For example:
import lotus.domino.*;
This statement tells the compiler to search the lotus.domino package. import statements can also specify a particular class in a package:
import com.libertastechnologies.utls.ACLSynch;
This is useful when you're importing separate packages that contain classes with the same name. Using an import statement to explicitly specify the desired class helps clear up any ambiguity.
Many people confuse import statements with #include directives in other languages. import statements don't actually import anything. They're more like PATH statements that inform the operating system where to look for programs on your hard disk.
AgentBase and NotesMain()
To make a Java agent, you create a class that extends Domino's AgentBase class. This ensures that your agent initializes the Notes system because AgentBase extends the NotesThread class. NotesThread is discussed later in this chapter.
AgentBase then creates a Session object and an AgentContext object. Both are used to get runtime information about the agent's environment. AgentBase also takes care of things such as wiring the standard Java input and output streams to the Notes Java console and setting up a timer thread that can limit agent execution time according to administrator settings. After everything has been set up, AgentBase calls NotesMain() .
The NotesMain() method, like the main() method in Java applications, is where your program begins and ends. Simple agents often consist of just this method, although it's usually a good idea to split up larger amounts of code into several methods and classes.
NotesMain() is one of the few Java methods names I've seen that starts with a capital letter. Unfortunately, the folks at Lotus choose to ignore the lowercase method name convention. Perhaps they thought Notes deserved to be capitalized!
The Session Object
As was mentioned earlier, every Java program written for Domino needs a Session object. The Session object is the top of the Domino object hierarchy and provides the entry point for your Java programs. You can use the Session object to do a variety of things. For the most part, you will use it to access existing data, such as DbDirectory objects and Database objects. You will also use it to create independent objects such as DateTime , DateRange , Name , and NewsLetter . Use the Session object to access system properties such as the Notes version, current username, and platform. Session is also used to call global methods such as evaluate() and freeTimeSearch() that don't seem to fit anywhere else.
Each agent starts with its own Session object, which you can access from any method in your agent's class by using the getSession() method, as in this example:
Session l_nsCurrent= getSession();
You can then use the variable l_nsCurrent to call methods like this:
If (l_nsSession.isOnServer()) System.out.println("This code is running on the server: " + l_nsSession.getServerName()");
API Help
To learn the full story about Session or any other class in the NOI, do the following:
- In Domino Designer, choose Help/Context Help or press F1.
- Open the Java/CORBA Classes section.
- Open the Java Classes AZ section.
- Scroll down and click the class name, such as Session .
When the help system refers to the properties of a class, it means fields that can be accessed with get methods, as in getServerName() . The property is ServerName . Things aren't always so clear-cut , though. Methods such as getDatabase() are listed separately, presumably because they retrieve data outside the class in question.
You can also use the Info pane to bring up help on a class or method (see Figure 18.2):
- Click the Reference tab in the Info pane.
- Choose Notes Java from the pull-down list.
- Open lotus.domino and then Interfaces.
- Open Session or another class.
- Open Methods if you want to see the methods in the class.
- Click a class or method and press F1 (or click Help) to view its help page.
Figure 18.2. Use the Info pane to browse the classes and methods of the Notes Java API.
Most of the Notes classes are listed as interfaces in the Info pane, even though they're not really interfaces in the strict Java sense. From what I can tell, the folks who wired up the Info pane consider a class an interface when it wraps a C++ object and cannot be directly instantiated or extended. Classes that can be used like normal Java classes, such as AgentBase and NotesThread , are considered classes in the Info pane.
The AgentContext Object
Before NotesMain() is called, AgentBase creates an AgentContext object and initializes it with information specific to the current agent's execution environment. You access the AgentContext object using the Session object like this:
Session l_nsCurrent = getSession(); AgentContext l_nacCurrent = aSession.getAgentContext();
AgentContext is most often used to get a list of documents in the current database that must be processed . The documents that are considered unprocessed depend on the Which Documents(s) Should It Act On setting in the Programmer's pane. You can choose to process unread documents, selected documents, modified documents, all documents in the view, or all documents in the database. To further narrow the list, add search criteria with the Add Search button. To access the list in your code, use the getUnprocessedDocuments() method to retrieve a DocumentCollection object. Use this object to cycle through the list, processing each document as you see fit.
AgentContext can also be used to access a handful of other agent-specific properties. Use getCurrentDatabase() to retrieve the agent's database. Use getLastRun() to learn when the agent last ran, and getLastExitStatus() to find out how things turned out. The getSavedData() method returns a Document object that you can use to save data between runs of the agent. This special document won't appear in any view. Keep in mind, though, that this document is cleared every time you make changes to your agent.
Exception Handling
The last part of the sample code template is an exception handler:
catch(Exception e) { e.printStackTrace(); }
Any errors that happen in the preceding try block cause an immediate transfer to this handler. The variable e contains an Exception object that has been passed, or thrown, from the spot where the error occurs, whether it's in the same method or several methods deep in your code.
The printStackTrace() method writes a snapshot of the event to the Java console, which in Domino Designer can be found in File, Tools, Show Java Debug Console. For example, this method makes an illegal call to Session.createDateTime() :
public void makeEntry() throws NotesException { Session l_nsCurrent = getSession(); DateTime l_dtTwoWeeksFromNow = l_nsCurrent.createDateTime("two weeks from now"); // .. more processing }
The throws NotesException clause in the signature tells Java to send NotesException errors back to the calling method. If you don't use a throws clause, your method must handle all possible Notes exceptions.
Calling this method from NotesMain() results in this console output:
lotus.domino.NotesException at lotus.domino.local.Session.createDateTime(Session.java:736) at JavaAgent.makeEntry(JavaAgent.java:21) at JavaAgent.NotesMain(JavaAgent.java:11) at lotus.domino.AgentBase.runNotes(AgentBase.java:160) at lotus.domino.NotesThread.run(NotesThread.java:202)
The indented lines are the stack trace in reverse order. Parentheses contain the source files and line numbers of each method call. Reading from bottom to top, you can see that NotesThread.run() called AgentBase.runNotes() , which, in turn, called the agent's NotesMain() method. These lines will appear in any agent stack trace.
The good stuff is in the next three calls. First, the makeEntry() method was called from NotesMain() on line 11 of the JavaAgent source file. Then the createDateTime() method was called from makeEntry() on line 21. The exception occurred within createDateTime() because it's the first method listed. The very top line is the exception message, which usually contains more information. This one isn't very helpful.
Unfortunately, exceptions are another area in which Notes strays from the Java way. Most exceptions describe themselves when you call getMessage() , which is what printStackTrace() uses to get the exception message. The NotesException class simply returns lotus.domino.NotesException , which tells you nothing.
Also, most non-Notes exceptions use class inheritance to organize exceptions into hierarchies. For example, Java's FileNotFoundException inherits from IOException , which, in turn, inherits from Exception . This is useful because handlers can be crafted to catch exceptions at any level in the tree. Notes, on the other hand, uses the sole NotesException class for its several hundred possible errors. Because NotesException uses error numbers instead of class types to distinguish exceptions, you must use if statements or switch statements within catch clauses, like this:
try { makeEntry(); } catch(NotesException e) { switch(e.id) { case NotesError.NOTES_ERR_NOTAFILE: System.err.println("Couldn't make the newsletter!"); break; case NotesError.NOTES_ERR_FILEOPEN_FAILED: System.err.println("Couldn't open file!"); break; default: System.err.println("Notes Exception " + e.id + " -- " + e.text); } } catch(Exception e) { e.printStackTrace(); }
Running this example displays the following:
Notes Exception 4468 -- Invalid date
Each NotesException object has an id field, which contains the Notes error number, and a text field, which contains the text description. The NotesError class holds all the error number constants, such as NOTES_ERR_NOTAFILE .
Also, note that catch clauses can be listed one after another. The first clause that matches the exception gets to handle it. Therefore, if a FileNotFoundException occurs in the preceding example, it skips the NotesException handler to the more general Exception handler.