Richard Pajerski  Software development and consulting

Part 3. Extending the GUI of native Notes apps with Java applets

by Richard Pajerski


Posted on Wednesday April 01, 2015 at 11:12AM in Tutorials


*** July 2018 UPDATE ***
Links to the final database and Java source are included at the bottom of this blog entry.

In Parts 1 and 2, we created a Java applet and incorporated it into a Notes database. In this last part, we're going to code the export functionality in our applet and refresh the applet in Notes.

===================================
Part 3 -- Export Notes data using the applet
===================================

Step 1. Program the Start button. Open the ContactExport class in NetBeans and at the top of the code editor, click the Design button for the visual representation of the applet. Then right-click on the Start button and then Events > Action > actionPerformed. NetBeans will create a method called jButton1ActionPerformed and place our cursor at the appropriate spot in the source where we can add code that executes when the button's clicked. So we start with:


private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
}

When we click the Start button, we want to immediately disable it so only one export operation runs at a time. This would normally just be the following one line of code:


jButton1.setEnabled(false);

However, in Swing, we need to make sure to put any code that updates the UI on the event dispatch thread. So we're going to disable the button in a new thread:


private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
try {
java.awt.EventQueue.invokeAndWait(new Runnable() {
public void run() {
jButton1.setEnabled(false);
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
}


Step 2. Create a separate thread to access our Notes database. Next, we're going to create a separate thread that will connect to Notes. For now, we're going to implement this as a private inner class inside our applet. Toward the bottom of our ContactExample class, add:


private class ExportRunner implements Runnable {

private Thread t;

@Override
public void run() {

}

public void start() {
t = new Thread(this);
t.start();
}
}



We're going to place most of our functional export code in the ExportRunner's run() method which is the method that gets called when we start a Java thread. Add the following block to the ExportRunner's run() method to access the "All Contacts" view and its documents:


@Override
public void run() {
try {
Session s = ContactExport.this.openSession();
Database db = s.getDatabase("", "Contacts.nsf");
View allContactsView = db.getView("AllContacts");
Document d = allContactsView.getFirstDocument();
while (d != null) {
// We access the data here...
Document tmpDoc = allContactsView.getNextDocument(d);
d.recycle();
d = tmpDoc;
}
allContactsView.recycle();
db.recycle();
} catch (NotesException nex) {
nex.printStackTrace();
}
}


Step 3. Cycle through the contact documents and save the contents to internal buffer. First, add a StringBuilder in the ExportRunner class which will serve as the internal buffer to store the comma-delimited data. Also declare a platform-independent line separator.


private class ExportRunner implements Runnable {
private Thread t;
private StringBuilder sb = new StringBuilder();
private String newline = System.getProperty("line.separator");



Back in ExportRunner's run() method, we next want to update the while loop to extract the contact field data and separate the fields with commas and a final carriage return at the end of each line:


while (d != null) {
String firstName = d.getItemValueString("FirstName");
String lastName = d.getItemValueString("LastName");
String phone = d.getItemValueString("Phone");
String city = d.getItemValueString("City");
sb.append(firstName);
sb.append(",");
sb.append(lastName);
sb.append(",");
sb.append(phone);
sb.append(",");
sb.append(city);
sb.append(newline);
Document tmpDoc = allContactsView.getNextDocument(d);
d.recycle();
d = tmpDoc;
}


Step 4. Update the progress bar as we go through the contacts. This may sound straightforward but it takes some setup. Let's start by adding these variables to the ExportRunner class, just below the StringBuilder declaration:


private class ExportRunner implements Runnable {
private Thread t;
private StringBuilder sb = new StringBuilder();
private String newline = System.getProperty("line.separator");
private int contactCount;
private javax.swing.Timer progBarTimer;



NetBeans' GUI builder will have already declared a JProgressBar for us using the variable name jProgressBar1 when we added the progress bar component to our JPanel back in Part 1. But to animate the JProgressBar, we need to create two additional items: a handler class and timer class. We're going to create the handler class as a new private inner class in the ExportRunner class. We'll call it ProgressBarHandler:


private class ProgressBarHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
int v = contactCount;
if (v != 0) {
jProgressBar1.setValue(v);
}
}
}


And we'll instantiate the Timer class by inserting this as our first line in the run() method of our ExportRunner class:


progressBarTimer = new javax.swing.Timer(2, new ProgressBarHandler());

The Timer takes our handler class as a parameter and, once started, will call its actionPerformed method every two milliseconds. In actionPerformed, we explicitly set the progress bar's value. When the thread starts, we start the Timer, prime the progress bar with starting and ending values and then update those values as we loop through the documents.

We're also going to add a new method to ExportRunner called isDone() that holds a boolean variable indicating when the export is complete. We set the boolean variable to true when we're done processing the documents and when the handler sees that, it sets the progress bar back to zero and stops the Timer. It also takes care of re-enabling our Start button.

Beyond the progress bar animation, a good GUI practice is to let the user know when the system is busy. We're going to do that with the following commands:


setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

Place the WAIT_CURSOR command outside the ExportRunner class – at the beginning of the jButton1ActionPerformed method and put the DEFAULT_CURSOR command as the last line in the handler class.

Here's the latest version of ExportRunner:


private class ExportRunner implements Runnable {

private Thread t;
private StringBuilder sb = new StringBuilder();
private String newline = System.getProperty("line.separator");
private int contactCount;
private javax.swing.Timer progressBarTimer;
private boolean done = false;

@Override
public void run() {

try {
progressBarTimer = new javax.swing.Timer(2, new ProgressBarHandler());

Session s = ContactExport.this.openSession();
Database db = s.getDatabase("", "Contacts.nsf");
View allContactsView = db.getView("AllContacts");

// Set the progress bar's minimum and maximum values.
jProgressBar1.setMinimum(0);
// Total contact documents in our view.
jProgressBar1.setMaximum(allContactsView.getEntryCount());

// Start the timer.
progressBarTimer.start();

Document d = allContactsView.getFirstDocument();

while (d != null) {
Thread.sleep(1); // Pause this loop to give other threads a chance to run.
contactCount++; // Increment the number of contacts processed.

String firstName = d.getItemValueString("FirstName");
String lastName = d.getItemValueString("LastName");
String phone = d.getItemValueString("Phone");
String city = d.getItemValueString("City");
sb.append(firstName);
sb.append(",");
sb.append(lastName);
sb.append(",");
sb.append(phone);
sb.append(",");
sb.append(city);
sb.append(newline);
Document tmpDoc = allContactsView.getNextDocument(d);
d.recycle();
d = tmpDoc;
}
allContactsView.recycle();
db.recycle();

} catch (NotesException nex) {
nex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
done = true;
}
}

public void start() {
t = new Thread(this);
t.start();
}

private boolean isDone() {
return done;
}

private class ProgressBarHandler implements ActionListener {

@Override
public void actionPerformed(ActionEvent event) {
int v = contactCount;
if (v != 0) {
jProgressBar1.setValue(v);
}
if (ExportRunner.this.isDone()) {
progressBarTimer.stop();
jProgressBar1.setValue(0);
jButton1.setEnabled(true);
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
}
}



Finally, to instantiate and run this new class when the Start button is clicked, go back to jButton1ActionPerformed and add these two lines to the run() method:

ExportRunner exportRunner = new ExportRunner();
exportRunner.start();

The actionPerformed method should now look like:


private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
jButton1.setEnabled(false);
ExportRunner exportRunner = new ExportRunner();
exportRunner.start();
}


Step 6. Store the output after prompting the user for a location. Our data is now in our StringBuilder object but in Java, it takes a few steps to get that data into a file. For additional flexibility, we're also going to prompt the user for a file name and location using a JFileChooser. Put this block just after the db.recycle() command in ExportRunner's run() method:


JFileChooser jFileChooser = new JFileChooser();
jFileChooser.setSelectedFile(new File("ExportedContacts.txt"));
int save = jFileChooser.showSaveDialog(ContactExport.this);

if (save == JFileChooser.APPROVE_OPTION) {
FileOutputStream stream = null;
PrintStream out = null;
try {
File file = jFileChooser.getSelectedFile();
stream = new FileOutputStream(file);
out = new PrintStream(stream);
out.print(sb.toString());
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
if (stream != null) {
stream.close();
}
if (out != null) {
out.close();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}



That's it for the code. Now recompile the applet and refresh it in Notes (see Step 5 of Part 2). Populate the Notes database with some sample contact data and try it out.

Conclusion
Embedding custom Java applets in the Notes client is a powerful way of extending the value of Notes client applications. It's a little more involved than building out similar functionality with traditional Notes design elements but it offers benefits that just aren't available in Notes. Considering the size of the Java API and availability of third party Java-based tools, this example only scratches the surface of the possibilities.

Files and source code
Contacts.nsf.zip
ContactExport-src.zip