An API's looks can be deceiving

Penetrate the hidden complexities that spoil simple conceptual models of Java APIs

When initially learning an API, a programmer rapidly forms a conceptual model of the implementation behind the interface. Unfortunately, programmers' preliminary models often represent the shortest distance from problem domain to solution. When the underlying implementation differs from your simple conceptual model, a trap awaits you.

In this article, you will examine two simple traps that many programmers stumble upon the first time they work with APIs. Hidden complexities in the implementation often muddy a simple conceptual model, thereby causing a trap. This month, you will uncover two examples of the model-implementation mismatch: the first when creating a Swing GUI and the second when finding a specific node in a Document Object Model (DOM).

Pitfall 7: When requestFocus() fails

You can validate a form's text field in several different ways. One approach is to validate the text field after the component has lost cursor focus. You accomplish that by registering a FocusListener with the component and checking the field value in the focusLost() method, as Listing 7.1, BadRequestFocus.java, demonstrates:

(Note: You can download all the source code that accompanies this article from Resources.)

Listing 7.1: BadRequestFocus.java

package com.javaworld.jpitfalls.article4;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class BadRequestFocus extends JFrame implements FocusListener
{
   public static final String NAME = "Name";
   public static final String AGE = "Age";
   public static final String BIRTHDAY = "Birthday";
   
   JTextField nameTF = new JTextField(20);
   JTextField ageTF = new JTextField(5);
   JTextField birthdayTF = new JTextField(12);
   JTextField status = new JTextField(40);
   HashMap map = new HashMap();
   
   public BadRequestFocus()
   {
      getContentPane().setLayout(new BorderLayout(2,2));
      Panel northP = new Panel(new FlowLayout(FlowLayout.CENTER));
      Panel centerP = new Panel(new GridLayout(3,1));
      Panel southP = new Panel(new FlowLayout(FlowLayout.LEFT));
      getContentPane().add("North", northP);
      getContentPane().add("Center", centerP);
      getContentPane().add("South", southP);
      
      // set up North Panel
      northP.add(new JLabel("Request Focus Tester"));
         
      // set up Center Panel
      Panel p1 = new Panel(new FlowLayout(FlowLayout.LEFT));
      p1.add(new JLabel(NAME));
      p1.add(nameTF);
      centerP.add(p1);
      Panel p2 = new Panel(new FlowLayout(FlowLayout.LEFT));
      p2.add(new JLabel(AGE));
      p2.add(ageTF);
      centerP.add(p2);
      Panel p3 = new Panel(new FlowLayout(FlowLayout.LEFT));      
      p3.add(new JLabel(BIRTHDAY));
      p3.add(birthdayTF);
      centerP.add(p3);
      
      // set up the South Panel
      status.setEditable(false);
      status.setText("Ready.");
      southP.add(status);
      
      // Frame only
      addWindowListener(new WindowAdapter()
                    {
                     public void windowClosing(WindowEvent wevt)
                     {
                        System.exit(0);   
                     }
                    });
                    
      // add this class as a FocusListener to the TextFields
      nameTF.addFocusListener(this);
      nameTF.setRequestFocusEnabled(true);
      ageTF.addFocusListener(this);
      ageTF.setRequestFocusEnabled(true);
      birthdayTF.addFocusListener(this);      
      birthdayTF.setRequestFocusEnabled(true);
      // map the components to a field name for validation
      map.put(nameTF, NAME);
      map.put(ageTF, AGE);
      map.put(birthdayTF, BIRTHDAY);
                          
      // show the window              
      setLocation(100,100);
      pack();
      setVisible(true);
   }
   
   public void focusGained(FocusEvent fevt)
   {
      System.out.println("Focus Gained.");      
   }
   
   public void focusLost(FocusEvent fevt)
   {
      Component c = fevt.getComponent();
      if (c instanceof JTextField)
      {
         JTextField tf = (JTextField) c;
         String val = tf.getText();
         // get the field name         
         String fieldName = (String) map.get(tf);
         if (fieldName != null)
         {
            if (fieldName.equals(AGE))
            {
               try
               {
                  int i = Integer.parseInt(val);
                  status.setText("Age is Valid.");
               } catch (NumberFormatException nfe)
                 {
                  // FAILED VALIDATION
                  // empty field
                  tf.setText("");
                  // get the focus back to try again
                  tf.requestFocus(); 
                  status.setText("Failed Field Validation. 
Re-enter.");                  
                 }
            }            
            else if (fieldName.equals(BIRTHDAY))
            {
               // validate the date...   
            }
         }
         else
            status.setText("Unable to Validate. Unknown field");
      }
   }   
   
   public static void main(String args[])
   {
      try
      {
         new BadRequestFocus();      
      } catch (Throwable t)
        {
         t.printStackTrace();
        }
   }
}

When Listing 7.1 executes, the program displays the GUI shown below:

Request focus tester

While most of the code (the BadRequestFocus constructor) creates the user interface (UI), the pitfall lies in the focusLost() method, which tries to request focus inside the focus notification.

In focusLost(), you first retrieve the field name associated with the JTextField so you know how to validate the field. That is done via the map HashMap. In the example above, I validate only the age field and validate that the value entered in the field is an integer. If the validation fails, I set the text field to an empty string and call requestFocus() to return the cursor to the component I am validating. Unfortunately, due to the complexity of event handling in a Swing UI, requestFocus() does not in fact obtain the focus when called inside the lostFocus() method.

You cannot request an event at any time without regard to the current states of both the Swing interface and the event-dispatcher thread. Luckily, we don't need to examine the Swing implementation source code to find a solution to this problem; Swing provides a utility method called invokeLater() that executes code on the event-dispatching thread at a later time. In our case, making use of this method properly invokes the requestFocus() method. Listing 7.2, GoodRequestFocus.java, uses that approach:

Listing 7.2: GoodRequestFocus.java

package com.javaworld.jpitfalls.article4;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
  
class FocusRequester implements Runnable
{  
   private Component comp;
  
   public FocusRequester(Component comp)
   {
      this.comp = comp;
      try 
      {
         SwingUtilities.invokeLater(this);
      } catch(Exception e) 
        {
         e.printStackTrace();
        }
   }
   
   public void run()
   {
      comp.requestFocus();
   }
}
public class GoodRequestFocus extends JFrame implements FocusListener
{
   // data members identical to BadRequestFocus class ...
   public GoodRequestFocus()
   {
        // this code identical to BadRequestFocus() ...
   }
   
   public void focusGained(FocusEvent fevt)
   {
      System.out.println("Focus Gained.");      
   }
   
   public void focusLost(FocusEvent fevt)
   {
      Component c = fevt.getComponent();
      if (c instanceof JTextField)
      {
         JTextField tf = (JTextField) c;
         String val = tf.getText();
         // get the field name         
         String fieldName = (String) map.get(tf);
         if (fieldName != null)
         {
            if (fieldName.equals(AGE))
            {
               try
               {
                  int i = Integer.parseInt(val);
                  status.setText("Age is Valid.");
               } catch (NumberFormatException nfe)
                 {
                  // FAILED VALIDATION
                  // empty field
                  tf.setText("");
                  status.setText("Failed Field Validation. 
Re-enter.");
                  // get the focus back to try again
                  new FocusRequester(c); 
                 }
            }            
            else if (fieldName.equals(BIRTHDAY))
            {
               // validate the date...   
            }
         }
         else
            status.setText("Unable to Validate. Unknown field");
      }
   }   
   
   public static void main(String args[])
   {
      try
      {
         new GoodRequestFocus();      
      } catch (Throwable t)
        {
         t.printStackTrace();
        }
   }
}

The key that makes the focus request work is the added FocusRequester class, which invokes SwingUtilities.invokeLater(). The FocusRequester class implements Runnable and thus has a run() method that calls requestFocus() on the Component passed in to its constructor.

The SwingUtilities.invokeLater() method only requires a Runnable object and executes that object's run() method on the Swing event-dispatcher thread. In the lostFocus() method, I replaced the call to requestFocus() with an instantiation of a FocusRequester object. That change results in focus returning successfully to the field that failed validation.

While we might like to be able to call any methods at any point in a program, such a simple approach fails with complex implementations like the Swing event dispatcher. The solution is to use the built-in SwingUtilities class to interact with the event dispatcher.

Pitfall 8: When a search for a DOM element comes up empty

All well-formed XML files have a tree structure. For example, Listing 8.1, myaddresses.xml, can be represented by a tree with two ADDRESS nodes:

Listing 8.1: myaddresses.xml

<?xml version="1.0"?>
<!DOCTYPE ADDRESS_BOOK SYSTEM "abml.dtd">
<ADDRESS_BOOK>
             <ADDRESS>
                         <NAME>Joe Jones </NAME>
                         <STREET>4332 Sunny Hill Road 
</STREET>
                         <CITY>Fairfax</CITY>
                         <STATE>VA</STATE>
                         <ZIP>21220</ZIP>
             </ADDRESS>
             <ADDRESS>
                         <NAME>Sterling Software 
</NAME>
                         <STREET> 7900 Sudley 
Road</STREET>
                         <STREET> Suite 500</STREET>
                         <CITY>Manassas</CITY>
                         <STATE>VA </STATE>
                         <ZIP>20109 
</ZIP>                         
             </ADDRESS>
</ADDRESS_BOOK>

Often, XML beginners wrongly assume that a DOM tree will look exactly like their mental image of the corresponding XML document. Let's say you must find the first NAME element of the first ADDRESS in Listing 8.1. By looking at the XML, you might think that the DOM's third node is the one you want. Listing 8.2 attempts to find the node in that way:

Listing 8.2: BadDomLookup.java

package com.javaworld.jpitfalls.article4;
import javax.xml.parsers.*;
import java.io.*;
import org.w3c.dom.*;
public class BadDomLookup
{
   public static void main(String args[])
   {
      try
      {
         if (args.length < 1)
         {
            System.out.println("USAGE: " + 
            "com.javaworld.jpitfalls.article4.BadDomLookup xmlfile");
            System.exit(1);
         }
         
         DocumentBuilderFactory dbf = 
DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse(new File(args[0]));
         
         // get first Name of first Address
         NodeList nl = doc.getElementsByTagName("ADDRESS");
         int count = nl.getLength();
         System.out.println("# of \"ADDRESS\" elements: " + count);
         
         if (count > 0)
         {
            Node n = nl.item(0);
            System.out.println("This node name is: " + 
n.getNodeName());
            // get the NAME node of this ADDRESS node
            Node nameNode = n.getFirstChild();
            System.out.println("This node name is: " + 
nameNode.getNodeName());
         }         
      } catch (Throwable t)
        {
         t.printStackTrace();
        }
   }
}

The simple program BadDomLookup uses the Java API for XML Processing (JAXP) to parse the DOM (I tested this example with both Xerces 1.3 and Sun's default JAXP parser). After you obtain the W3C Document object, you retrieve a NodeList of ADDRESS elements and then look to obtain the first NAME element by accessing the first child under ADDRESS.

When you execute Listing 8.2, this is what you get:

C:\classes\com\javaworld\jpitfalls\article4>java 
com.javaworld.jpitfalls.article4.BadDomLookup myaddresses.xml
# of "ADDRESS" elements: 2
This node name is: ADDRESS
This node name is: #text

The result clearly shows that the program has failed to accomplish its task. Instead of an ADDRESS node, you've received a text node. What happened?

1 2 Page 1
Page 1 of 2