Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

How Do I Build a Find?


Jul00: Java Q&A

David is a Sun-certified instructor and Java programmer who develops CORBA-based client-server software at NetGenics. He can be contacted at [email protected].


I was saddled with the task of implementing "find" functionality for an aspect of our product, and since we hadn't yet introduced the capability to do a find elsewhere in the product, I was starting from scratch. I thought it would be a pretty easy thing to locate this kind of functionality on the Internet -- I mean, every product allows a find or search. Because I was working in Java, I posted a query to c.l.j.p and I hunted around cyberspace. I didn't have a lot of time, and what I found was that there wasn't a lot on find to find.

Backed into a corner, I opted to "find" my own way out. I began by examining what users expect when they do a find. There are basically two flavors. One is the type exemplified by searching for files in Windows Explorer, where the search criteria are specified in one window, the search is conducted, and all matches are displayed at once in another drillable view. The other kind is seen when searching for a pattern in a text document, where a single different match in the text is highlighted for each invocation of find or find next/previous. It seemed like we would eventually need both types, so I decided to design a find functionality to cover both behaviors.

One of the first steps I took was to divide the functionality of conducting a find into two stages, breaking it naturally between input and output. The input stage is where users are prompted to supply the criteria by which the search should be conducted, but before the search has been kicked off. Once users press the Find button, the output stage commences. During this stage, the search is conducted and the results of it are handed over to a class that knows how to handle the results.

To do this successfully, I relied on the power of Java interfaces. I didn't want to force users to use any special graphical components to prompt them to enter search criteria or to display the results of a search. Accordingly, I designed an interface called FindPrompt that input classes implement, and one called FindResult that output classes implement. If both the input and output pieces are to be graphical (which is most likely), they can be anything descendent from java.awt.Component. But they don't both have to be graphical. The assumptions I made were that:

  • The prompting piece (but not necessarily the result-handling piece) will be a visible graphical component of some type where the user can specify the criteria by which to conduct the search.
  • The Find dialog will show the prompting piece together with a minimum set of buttons comprised of a Find button and Close button.

If the results of the search were also being shown in a graphical view, then the Find dialog would build itself to include that view along with the view making up the prompt and the buttons.

Figure 1 is a UML diagram of the Find dialog and the interfaces it makes use of. The FindDialog class holds references to objects implementing the FindPrompt, FindResult, and Searchable interfaces. One of the keys to making this design so flexible is that these are very simple interfaces, each with one to three methods. In brief they are:

  • The Searchable interface, which has only one method -- the search method -- that takes an instance of Transferable as the search criteria, and returns a bidirectional ListIterator over the results of the search. Because this uses Transferable as the search criteria, virtually anything could be the search criteria, and the same criteria could be used to conduct different types of searches.

  • The FindPrompt interface, which has one method of its own and inherits the three declarations of the java.awt.datatransfer.Transferable interface. Thus, after the user completes specifying the criteria to search for, the program can simply give the FindPrompt itself to Searchable as the argument to the search method. The getPromptComponent method is used to give the FindDialog a handle to the graphical component that constitutes the prompt.

  • The FindResult interface, which has three methods of its own. The getResultComponent method is used to give the FindDialog a handle to the graphical component that constitutes the result handler, if there is to be one. This may return null if the result handler is implemented in some nonvisual class separate from the FindDialog itself. The setSearchResult method is the first notification the result-handling class gets that the search has been conducted. The bidirectional iterator over the results is given to the result-handling class in this method. The wantsNextPreviousBehavior simply returns True or False depending on whether the result-handling view wants the FindDialog to display the Next and Previous buttons.

The Transferable interface was designed to be part of the mechanism for transferring data between Java and system clipboards, possibly across different Java Virtual Machines. What's particularly interesting about this interface is that client classes can interrogate the Transferable instance to discover what types of data are being transferred. The mime types (or DataFlavors in Java-speak) that are supported right out of the box by the JDK include Strings and plain text, but you can add your own DataFlavor easily enough: The DataFlavor constructor takes a class representing the mime type.

What this means is that the search criteria can be any class implementing Transferable. The methods you need to implement are getTransferData(DataFlavor), which returns an object representing the transferable data itself; getTransferDataFlavors(), which returns an array of DataFlavor objects indicating the flavors in which the data can be provided; and isDataFlavorSupported(DataFlavor), which returns whether the specified DataFlavor is supported for this object. A single search criterion might support more than one DataFlavor. For example, the string "9/24/99" could be represented as a String DataFlavor, or as a Date DataFlavor, and consequently could support a choice of methods by which to conduct a search.

The example I discuss looks at the case where a user is interested in iterating over a string, looking for a pattern to recur in that string. Figure 2 is an example implementation. The user starts this application, innovatively called "FindTest," and passes it the name of a text file as an argument to main. The text is loaded from the file into a large central text area (actually a subclass of java.awt.TextArea called FileTextArea). The FileTextArea implements FindResult: It will show the user each match for the search pattern by highlighting the matches one at a time as the user presses Next or Previous to iterate through the matches.

The text in this FileTextArea becomes the Searchable object. I'll show how this is done using a class called SearchableStringAdapter that implements the Searchable interface for Strings. I created this class to easily convert Strings into Searchable objects because String searching is so common.

Above the FileTextArea is a subclass of java.awt.TextField called TextFieldPrompt, which implements FindPrompt and serves as the graphical prompt where users enter the pattern to be searched for. By implementing FindPrompt, this class also needs to implement the inherited Transferable interface. It does so by representing that the TextFieldPrompt supports the DataFlavor.stringFlavor type of DataFlavor.

Below the FileTextArea are the buttons users hit to conduct the search. In this example, the class implementing FindResult (FileTextArea) returns True to the wantsNextPreviousBehavior call, so the Next and Previous buttons appear.

Interfaces to Interfaces

FindDialog needs to know when to enable or disable the buttons of its interface. The Find button should be enabled as soon as there are valid search criteria, so it needs to listen to the FindPrompt to know when to enable. The FindDialog also needs to listen to the FindResult, because the FindResult object keeps track of the state of the search by interrogating the result iterator. The iterator in the FindResult can inform you of search conditions for each of the following cases:

  • The Previous button does not enable until the user has moved beyond the first match.
  • The Previous button must disable if, at any time, the user goes back to the first item.

  • The Next button needs to disable if, at any time, the user has reached the end of the search results.

  • The Next button should enable if the user moves to any item before the last item.

To convey the state of the search, I created an event subsystem intended to help with enabling the interface. I fire off an instance of the FindEnabledEvent class whenever a change to the find state might affect the interface. This event class principally wraps three Boolean values: canFind, canFindNext, and canFindPrevious. When users enter text in the TextFieldPrompt, for example, the TextEventListener that is registered with the TextField (see Listing One) picks up the change in text. It then turns around and fires off an event to let the FindDialog know if it should enable the Find button based on whether there is something valid in the TextField. A similar behavior takes place in the result-handling class, where every movement of the search iterator might result in a change to the FindDialog interface.

In some cases, however, the result handler may not give a hoot about the next or previous activity, especially if it is handling the results of a Windows Explorer type of find (where all the search results are shown in their entirety and there is no such thing as the next or previous result). In this case, it doesn't make sense to register the FindDialog as a listener for FindEnabledEvents coming from the FindResult. Additionally, the ongoing find may be influenced by sources other than just the prompting and result-handling pieces. The state of a find might be invalidated because another user has deleted some records in a database, but the classes involved are neither the FindPrompt nor the result handler.

Because of this variability as to whether a class is a source of FindEnabledEvents, there needs to be a way for classes to identify themselves as generators of the FindEnabledEvent. To do this, I created an interface called FindEnabler, which acts as a kind of tag to identify implementing classes as generators of FindEnabledEvents. As such, it has methods to let client classes add and remove themselves as listeners to FindEnabler objects. Figure 3 shows a UML diagram of the classes and interfaces involved in enabling or disabling the FindDialog interface. FindDialog implements the FindEnabledListener interface so it can receive events when there are changes in the state of the find. To know who to listen to, the FindDialog can ask if an object is an instance of the FindEnabler interface; and if so, the FindDialog knows it can call the addFindEnabledListener method on that object. By default, the FindDialog tries to register itself as a FindEnabledListener with the FindPrompt and the FindResult, as in Listing Two. Other classes that might affect the state of the find also can implement FindEnabler and have the FindDialog register with them.

Performing Finds

Now that you can synchronize the interface with the state of the search as it is being readied or conducted, let's see how it works the other way around: How do the buttons on the FindDialog interface cause search activity to take place? Again, I introduced an event-handling mechanism to do the necessary notifications, creating a subclass of java.util.EventObject called FindActionEvent. This class doesn't add any data or methods to EventObject -- I did the subclassing just to get the type specificity.

This specialized event is delivered via the FindActionListener interface, which declares four methods that listeners can implement to receive notifications when the user interacts with the dialog. The FindResult class implements this interface and can be registered with the FindDialog to receive callbacks. The four methods of the FindActionListener interface are straightforward enough: findPerformed, findNextPerformed, findPreviousPerformed, and findAllPerformed. The event handlers registered with the appropriate buttons in the FindDialog GUI take button clicks and generate FindActionEvents.

For example, look at Listing Three. When the Find button is pressed, the search is conducted and its resulting iterator is given to the FindResult using the setSearchResult method. Then, after the FindResult has been given the result iterator, the findPerformed method is called. Notice that I am passing TextFilePrompt as the source of the FindActionEvent when calling findPerformed. This way, in FileTextArea's event handler for findPerformed, I can ask the prompt for the search's string pattern so the matches for that pattern can be highlighted.

Notice that the implementation of findPerformed taken from the FileTextArea (Listing Four) stores the String pattern that is being searched for. If there are results, the event handler sets the direction in which the search is moving to forward. This is significant because for the iterator to point to the correct next or previous instance of the pattern, it needs to be moved an extra iteration if the user changes direction. You can see the code checking for changes in search direction in the handlers for the findNextPerformed and findPreviousPerformed methods.

Next, the handler highlights the first occurrence of the pattern in the text of the FileTextArea. Finally, it uses the result iterator to determine what elements of the FindDialog interface should be enabled. The FileTextArea class has data members to flag whether the iterator will support a call to Next or Previous. The enableInterface method (see Listing Five) uses these class members to set the state of the canFindNext and canFindPrevious members in the FindEnabledEvent that it passes to the FindDialog.

Finds from Above

Let's take a birds-eye look at the way main is set up; see Listing Six. The TextFieldPrompt is set up as the implementor of the FindPrompt interface, and the FileTextArea as the implementor of the FindResult interface. The text in the FileTextArea is also turned into the implementor of the Searchable interface. main then gives the objects implementing these three important interfaces to the FindDialog, and calls show on the FindDialog. To set up the GUI, the FindDialog uses the getPromptComponent method to ask the FindPrompt for the instance of java.awt.Component that should be put in the northern region of the dialog. It then asks the FindResult for its component. If there is one, it gets added to the central region of the dialog. The FindPrompt GUI component, of course, is the TextFieldPrompt, and the FindResult GUI component is the FileTextArea. The FileTextArea has already been populated using the text in the file named as an argument to main. The southern region of the dialog is populated with the buttons needed to operate the dialog.

To build the Searchable, main grabs the text out of the FileTextArea and creates a SearchableStringAdapter. The SearchableStringAdapter is a class that makes it simple to search Strings by calling the search method of the Searchable interface, passing in an instance of Transferable that supports the stringFlavor DataFlavor, and getting back an iterator over the results. Results are stored in an ArrayList collection of Integers, each pointing to a location in the Searchable string where the pattern starts. The SearchableStringAdapter class lets you do several things, including specifying whether to do case-sensitive searches, changing the target string that gets searched without changing the pattern being searched for, or changing the pattern being searched for without changing the target. The code for this article (available electronically; see "Resource Center," page 5) also includes a class called TransferableStringAdapter that can be used to easily convert Strings into Transferable objects.

This is a flexible framework for building searches that divides the process into an input stage and output stage. Flexibility comes from widespread use of lightweight interfaces, the lack of insistence on specialized components, and a loosely coupled event-driven notification system to keep the GUI and the search process synchronized. The only graphical components are those that happen to make up the example, namely the Find dialog itself and some of its buttons. But users of this framework could easily build a different GUI -- one based in Swing components, for example. Outside of this, the graphical components representing the search's inputs and outputs can be any classes that have java.awt.Component in their ancestry. Finally, this part of the framework would be made even more generic by specifying that the input and output representations have no parental restrictions other than java.lang.Object.

Acknowledgment

Thanks for contributions from the software architecture group at NetGenics.

DDJ

Listing One

// java.awt.event.TextListener interface implementation
    /** Whenever the text value changes, let any registered
     * FindEnabledListeners know.
     */
    public void textValueChanged(TextEvent event)
    {
        // Create a FindEnabledEvent with this as its source. Set the find 
        // state equal to whether there is something to find or not and fire 
        // the event off to all FindEnabledListeners.
        FindEnabledEvent evt = new FindEnabledEvent(this);
        String str = this.getText();
        evt.setCanFind(str.length() > 0);
        for(int i = 0; i < findEnabledListeners.size(); ++i)
        {
            FindEnabledListener listener = 
                (FindEnabledListener)findEnabledListeners.elementAt(i);
            listener.findChanged(evt);    
        }
    }

Back to Article

Listing Two

// Register this FindDialog to listen to result handler for enabling events.
        if(findResult instanceof FindEnabler)
            ((FindEnabler)findResult).addFindEnabledListener(this);
        // Register this FindDialog to listen to the find prompt for
        // enabling events.
        if(findPrompt instanceof FindEnabler)
            ((FindEnabler)findPrompt).addFindEnabledListener(this);

Back to Article

Listing Three

// Search the Searchable using the Transferable, and give search results to 
//  the result handler.  Notify listeners. This is where search starts.
        findBtn.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent evt)
            {
                try {
                    // Conduct the search
                    ListIterator iter = searchable.search(findPrompt);
                    findResult.setSearchResult(iter);
                    // Notify listeners
                    FindActionEvent event = 
                           new FindActionEvent(FindDialog.this.findPrompt);
                    for(int i = 0; i < findActionListeners.size(); ++i)
                    {
                      FindActionListener listener =
                        (FindActionListener)findActionListeners.elementAt(i);
                      listener.findPerformed(event);    
                    }
                } catch(UnsupportedFlavorException ex){
                    System.out.println("Couldn't search " + searchable 
                                       + " using " + findPrompt);
                }
            }
        });

Back to Article

Listing Four

public void findPerformed(FindActionEvent event)
    {
        // Store off the String that I am searching for
        try
        {
            FindPrompt prompt = (FindPrompt)event.getSource();
            pattern = (String)prompt.getTransferData(DataFlavor.stringFlavor);
        } 
        catch(Exception ex)
        {
            // needs to handle cast, io, interrupted exceptions
        }
        if(resultIter.hasNext())
        {
            forward = true;
            Integer integer = (Integer)resultIter.next();
            this.select(integer.intValue(), integer.intValue() + 
                                                         pattern.length());
            hasPrevious = false;
            hasNext = resultIter.hasNext();
            this.enableInterface();
            requestFocus();
        }
    }

Back to Article

Listing Five

protected void enableInterface()
    {
        // Send event to FindDialog
        FindEnabledEvent event = new FindEnabledEvent(this);
        event.setCanFindNext(hasNext);
        event.setCanFindPrevious(hasPrevious);
        for(int i = 0; i < findEnabledListeners.size(); ++i)
        {
            FindEnabledListener listener =
                (FindEnabledListener)findEnabledListeners.elementAt(i);
            listener.findChanged(event);    
        }
    }

Back to Article

Listing Six

public class FindTest
{
    // Pass in name of text file to load into
    // the FileTextArea.
    public static void main(String[] args)
    {
        Searchable searchable = null;
        FindPrompt prompt = null;
        FindResult result = null;
        try
        {
            prompt = new TextFieldPrompt(45);
            result = new FileTextArea(20, 45, args[0]);
            String str = ((TextArea)result).getText();
            searchable = 
                new SearchableStringAdapter(str, prompt, true);
            FindDialog dialog = 
                new FindDialog(new Frame(),prompt,result, searchable);
            dialog.pack();
            dialog.setVisible(true);
        }catch(UnsupportedFlavorException ex) 
        {
            System.out.println("can't search " + searchable + 
                                                    " with " + prompt);
        } catch(java.io.IOException ex)
        {
            System.out.println("can't search " + args[0] + ": " + ex);
        }
    }
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.