ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


BlackMamba: A Swing Case Study

by Ashwin Jayaprakash
03/10/2004

Developing a UI-based program is an excruciating experience. It takes years to get it right. The tried-and-tested approach is to develop it in layers (Remember "Onion-boy" from Shrek?). Today, everybody talks about MVCs and Singletons. So, I'll assume that you already know what MVC is. Implementing MVC looks straightforward until you sit down to write the code. Over the years I have refined my approach, which I'll explain momentarily. Note that there is no single way to do it right. I chose my method because it's simple and I believe in the "Principle of Parsimony", also known as "Occam's Razor".

My latest hobby project is a semi-automatic email previewer and spam filter that I call BlackMamba. It is a Swing-based application, fairly small and limited in its functionality. But it does its job well. It uses a library called Mail4Me to communicate with the POP3 server. So it is a "networked" application and that adds another layer of complexity. It also uses a lot of text files to load and store data: Mail accounts, Spammer list, Address book, etc. So you see that even designing such a small application requires a lot of aspects to be considered.

In this article we'll discuss how to develop a desktop application using many of the architectural principles in the proverbial Book of OOAD. BlackMamba, shown in Figure 1, will be our case study. We will also list some of the common pitfalls that one encounters when developing such an application in Java Swing and learn how to overcome them.

Click for larger view
Figure 1. BlackMamba in action. (You can click on the screenshot to open a full-size view.)

Layers

If you are a visual kind of person who prefers to use the mouse rather than the keyboard, you will find writing Java Swing code an arduous task.

Since we are following the MVC approach, we are told that the model can be developed independently and the control and view can later be sewed onto it. It has been my experience that form almost never follows function. It is the other way round. If your model has not already been developed, then consider creating a prototype on paper or a whiteboard to get a feel of the flow in the application and how the data has to be partitioned across screens. If your client has specified any User Interface Guidelines, the Paper Prototype helps to refine the flow, which reduces the changes required to the model and the view in later stages.

The Resources section at the end of this article contains links to all the tools and techniques described here.

Get accustomed to using a UI editor. NetBeans has a very good Swing editor. It's just a drag-and-drop affair. But there is one thing you must be wary of: generated code! After you create the UI, edit the code and remove all generated methods. Keep your UI free of any logic. Keep it stupid. Promote all your member fields in the classes to public. Now, I'm sure there are a lot of you who will raise an objection to this. Saying that this is anti-OOP. If you read a little further ahead, you will see my point.

Keep all your UI/view code in a separate package like blackmamba.ui. After the UI design, fire up your favorite Java editor (mine is Eclipse) and create a package for the control, such as blackmamba.ui.control. This is where you write all your UI logic. I like to name my control classes as verbs (Start, Login, Configure, etc.) and my view classes as nouns.

This kind of naming makes it easier to decide where to place your logic for each set of widgets participating in a UseCase. The view is completely free of code. Any changes to the layout or model can be done from the control. The UI contains no data that needs to be hidden. It is not something like a DepartmentBudget class whose budget field must be read-only to the CompanyBudgetReporter class, something accomplished by using a getter-only method and a private-access modifier for the budget field. For a widget, such security is not needed. In fact, there is not much security at all. The code below prints the class names of the entire UI tree. One can easily navigate to a JTextField and change its contents by casting the JComponent to JTextField and invoking the setText(...) method.

Container root = frame.getContentPane();
printClassNamesOfChildren(root);

void printClassNamesOfChildren(Container cont)
{
  for(int i=0; i < cont.getComponentCount(); i++) 
  {
    Component comp = cont.getComponent(i);
    if(comp instanceof Container) 
    {
      printClassNamesOfChildren((Container) comp);
    }
    else
    {
      System.out.println(
                       comp.getClass().getName());
    }
  }
}

The OO concept of data-hiding/encapsulation should be applied where it matters -- and it doesn't matter in the UI/view. Besides, look how ugly the code to access the widget's fields from the control would be: mailsPanel.getSpamMailsPanel().getList().add(...), as opposed to mailsPanel.spamMailsPanel.list.add(...). This technique is definitely not recommended for use anywhere outside the view. There is a Law of Demeter that says "Only talk to your immediate friends." This cannot be enforced completely in UI packages because the control classes have to know the internals of the view classes to be able to control them. A statement like mailsPanel.spamMailsPanel.list.add(...), where the mailsPanel is accessing the list's add(...) method (which is not directly available to the mailsPanel) would be a definite no-no in a non-UI context. Here, however, we are exempted.

The view is manipulated and managed entirely by the control. The view is not controlled from within but from the outside. To prevent any access to the UI from anywhere other than the control packages, a simple tool called Macker can be used to enforce such architectural rules. In the simple Macker configuration file shown below, access to view classes is denied to all classes except those in the control package and the view package itself.

<?xml version="1.0"?>
<macker>    
  <ruleset name="Layering rules">

    <pattern name="view"
     class="blackmamba.ui.view.**" />
    <pattern name="control" 
     class="blackmamba.ui.control.**" />
        
    <access-rule>
        <message>Only the Control can talk
                 to the View
        </message>

        <deny>
            <to pattern="view" />
            <allow> 
                <from pattern="view"/> 
            </allow>
            <allow>
                <from pattern="control"/>
            </allow>
        </deny>
    </access-rule>
       
  </ruleset>
</macker>

Control

The control classes control the entire lifecycle of the view classes. It is the control class' responsibility to instantiate the UI, hookup listeners, attach models, and finally dispose them after the operation. It is very easy to have bloated control classes with too much logic forced upon them. A good rule of thumb is to display and handle only those screens (view/noun) that are involved in completing the action indicated by the control/verb, and delegate the rest to the control class that is next in line.

For example, in BlackMamba:

Controller/Verb View/Noun
(Modified Hungarian Notation)
Description
Start AboutDlg, AboutWin, MambaFrm Launch main frame. Setup menu action listeners and delegate Login action.
Login MambaFrm, LoginDlg Launch login dialog. Setup button action listeners and delegate Connect and Accounts actions.
Connect SplashPnl Launch connect dialog. Connect to the server, update progress, and setup cancel button action listener
Fetch MailsPnl, MambaFrm, SplashPnl Launch fetch dialog. Connect to the server, fetch mails, update progress, submit mails to MailProcessor, and setup cancel button action listener.

As a general practice, try to split the logic across controllers. Invent more verbs to which the bloated controller can delegate control to. A controller with more than 500-600 lines is definitely bloated. Smaller classes also improve readability.

Responsive UI

If the UI performs operations that require communication across a network like a database call or remote server invocation, or any operation that requires a lot of time to complete, it is best to have a Cancel option. The network can fail, or the remote server can crash. When performing such lengthy operations, it must be done in a separate Thread. Swing has a dedicated Thread to perform UI operations like adding/removing components to the UI, painting the screen, listening for buttons clicks, and dispatching events to listeners.

An important thing to keep in mind is that the non-GUI Thread must not make any direct changes to the UI. A UI operation that requires execution by it must be submitted to the EventQueue using the SwingUtilities.invokeLater(runnable) or SwingUtilities.invokeAndWait(runnable). A way to find out if the Thread invoking your method is that dedicated UI Thread or not is by invoking javax.swing.SwingUtilities.isEventDispatchThread().

The following code is a control class to display a Thread-safe "Connection progress" view. The classes Setup, LiteThreadPool, and PoolThread are described later.

public class Connect
{
    //Note the "volatile" keyword.
    private volatile int progressStep;
    ...
    final Runnable target = new Runnable()
            {
                public void run()
                {
                    connect();
                }
            };
    ...
    ...
    dialog.setDefaultCloseOperation(
        javax.swing.WindowConstants \
                            .DO_NOTHING_ON_CLOSE);
    
    dialog.addWindowListener(
        new java.awt.event.WindowAdapter()
        {
            public void windowClosing(
                   java.awt.event.WindowEvent evt)
            {
                cancel();
            }
        });
    ...
    ... 
    LiteThreadPool threadPool = 
                        Setup.getLiteThreadPool();
    PoolThread thread1 = threadPool.getThread1();
    thread1.execute(target);
    ...
    ...
    protected void connect()
    {
        final Runnable runnable = new Runnable()
        {
            public void run()
            {
                //Update UI
                splashPnl.connectingPrg.setValue(
                                    progressStep);
            }
        };
    
        try
        {
            mailHelper.connect(accountDetails);
            progressStep = 1;
            SwingUtilities.invokeLater(runnable);
    
            mailHelper.getMessageCount();
            progressStep = 2;
            SwingUtilities.invokeLater(runnable);
    
            mailHelper.disconnect();
            progressStep = STEP_END;
            SwingUtilities.invokeLater(runnable);
        }
        catch (Exception e)
        {
            ...
        }
    }
    
    protected void cancel()
    {
        boolean success = true;

        try
        {
            if (progressStep != STEP_END)
            {
                mailHelper.immediateDisconnect();
                success = false;
            }
        }
        catch (Exception e)
        {
        ...
        }
    
        dispose(success);
    }
   ...
   ...
}

Never perform lengthy computations inside a listener like ActionListener's actionPerformed() method. Spawn a new Thread and do the operation in the new non-GUI Thread. If it is not done this way, then the button will stay clicked until the lengthy operation completes. All the windows will turn gray because they cannot be painted/refreshed by the AWT-Thread, which is stuck doing your time-consuming operation. Even if you have a cancel button, it will not respond to mouse-clicks because the AWT-Thread is the one that is supposed to listen for such events and then dispatch the event to the listeners. This deadlock freezes the UI.

If the UI does such multi-threaded operations frequently, then a Thread Pool is a good option to improve performance. Spawning a new Thread every time is a very expensive operation. It can be accomplished easily using just two classes. The Thread Pool starts the required number of Threads beforehand and keeps them ready for consumers. After using a Thread, it is returned to the pool for reuse.

public class LiteThreadPool
{
    private PoolThread thread1;
    private PoolThread thread2;
    
    public LiteThreadPool()
    {
         thread1 = new PoolThread();
         thread1.setDaemon(true);
         thread1.start();
    
         thread2 = new PoolThread();
         thread2.setDaemon(true);
         thread2.start();
    }
    
    //Thread1 used by MailProcessor.
    public PoolThread getThread1()
    {
         return thread1;
    }
    
    //Thread2 used by control classes.
    public PoolThread getThread2()
    {
         return thread2;
    }
} 

public class PoolThread extends Thread
{
    private Object lock = new Object();
    private Runnable target = null;

    public void run()
    {
    while (true)
    {
    
        synchronized (lock)
        {
            while (target == null)
            {
                try
                {
                    lock.wait();
                }
                catch (InterruptedException e)
                {
                    if (Setup.DEBUG) 
                    {
                        e.printStackTrace();
                    }
                }
            } //end inner while

            try
            {
                target.run();
            }
            //Note that Exception is being caught.
            //If not, RuntimeExceptions can get 
            //through and kill the PoolThread. 
            catch (Exception e)
            {
                if (Setup.DEBUG)
                {
                    e.printStackTrace();
                }
            }
            
            target = null;
            
        } //end sync
        
    } //end while
    }

    public void execute(Runnable r)
    {
        synchronized (lock)
        {
            target = r;
            lock.notifyAll();
        }
    }
}

If you read the LiteThreadPool and PoolThread classes closely, you will realize it is a very rudimentary form of Thread pooling. In fact, it is just reusing Thread1 for the MailProcessor and Thread2 for the control classes, which is sufficient for a small application like BlackMamba. There is actually no pool from which Threads are picked up for processing Runnable targets. If a caller wants its target to be executed by a Thread from the pool and the only Thread is already executing another Runnable, it has to wait for it to finish. A real Thread pool will maintain a data structure like a Synchronized Queue that will hold several pre-initialized PoolThreads. Requests will be serviced by removing PoolThreads from the Queue in FIFO order. Once the PoolThread completes executing the target, it is smart enough to add itself back to the Queue.

Asynchronous Processing

With the enormous power offered by Threads comes a vexing problem. It is not easy for these methods to return results because the invoker has already returned. The only solution is to have an object on which the Thread can invoke callback methods to convey the results of the asynchronous execution.

This presents an interesting design problem: Class Worker1 submits jobs to the ThreadPool class. For the ThreadPool to convey the results of the execution, it invokes a done(boolean) method on Worker1. The done() method returns false to indicate failure and true for success. Sometime later another class Worker2 decides to use asynchronous processing. For ThreadPool to invoke a callback method on Worker2 say, completed(boolean), it has to differentiate between Worker1 and Worker2.

If another class Worker3 comes up in the future, then the list of methods ThreadPool should invoke has to change again. ThreadPool is now subject to constant change and hence is not a stable class. It requires frequent changes and compilations. To alleviate this problem, we will use an interface called Callback, which will be implemented by all users of ThreadPool. This way ThreadPool becomes stable.

The principle employed here is called Dependency Inversion (DIP), which advocates a dependence on Abstractions and not Concretions. Interfaces and Abstract classes are usually stable; implementations and subclasses usually are not.

public interface Callback
{
    public void call();
}

Figure 2
Figure 2. Too much direct dependence on Concrete classes.

Figure 3
Figure 3. Dependence on a more stable Interface resulting from Inversion.

The next problem is to find a package in which to place the Callback interface. blackmamba.ui.control.mail.Fetch submits the mail to blackmamba.helpers.mail.MailProcessor for asynchronous processing. MailProcessor adds the processed mail to blackmamba.helpers.ui.CustomTableModel. When a new mail is added to the model, the view has to be updated. And we have decided at the very beginning that view classes can only be accessed from control classes. So, CustomTableModel must notify blackmamba.ui.control.mail.Mails when a new mail is added so that Mails can make changes to the UI, such as incrementing and displaying the number of mails that have arrived. Our first choice would be to place Callback in blackmamba.ui.mail.control. Let's look at the dependency here: blackmamba.ui.control.mail.Fetch -> blackmamba.helpers.mail.MailProcessor -> blackmamba.helpers.ui.CustomTableModel -> blackmamba.ui.control.mail.Callback <- blackmamba.ui.control.mail.Mails.

The arrow indicates the direction of dependency. There is a cyclic dependency here, which starts and ends at the blackmamba.ui.control.mail package. Because of the cycle, the packages involved in the cycle are forced to be released and reused together. The only way to break the cycle is to move Callback to another package blackmamba.helpers.Callback. A tool like Compuware's OptimalAdvisor can be used to explore package design, dependencies, and usages.

There will always be certain utility packages like blackmamba.helpers and blackmamba.data that get tangled up in cycles. The effort required to enforce DIP on these classes is just not always justifiable. So it is better to just let them be. Figures 4 and 5 show the resulting acyclic dependencies in the sub-packages of blackmamba.ui and blackmamba respectively. The numbers on the arrows describe the number of dependencies between the packages. There are 126 dependencies from helpers to data and 4 in the opposite direction.

Figure 4
Figure 4. Acyclic dependencies in the blackmamba.ui package.

Figure 5
Figure 5. Acyclic dependencies in the blackmamba package.

Everytime the user clicks the Fetch button, new mails are read from the server. These mails are read by Thread2 from the ThreadPool and the UI is updated in a way very similar to the Connect class. This Thread sends the mails to the MailProcessor, which stores them in an internal Queue. MailProcessor uses Thread1 from the ThreadPool to pick up mails from the Queue and classify them. Everytime a mail is processed, it is added to the appropriate MailList, which as you'll notice, is implemented by the CustomTableModel. The interface is used here because MailProcessor does not have to know about other methods in the CustomTableModel.

public class Fetch
{
    public void init(MambaFrm frm, MailsPnl pnl, 
                                      boolean all)
    {
        ...

        final Runnable target = new Runnable()
        {
            public void run()
            {
                fetch();
            }
        };
        ...
        ...
        LiteThreadPool threadPool = 
                       Setup.getLiteThreadPool();
        PoolThread thread1 = 
                         threadPool.getThread1();
        thread1.execute(target);
        ...
    }
        
    ...
    protected void fetch()
    {
        ...
        MailProcessor mailProcessor = 
                         Setup.getMailProcessor();
        ...
        mailProcessor.process(mailDetails);
        ...
    }
}

public class MailProcessor
{
    private MailList mails;
    private MailList possibleSpamMails;
    private MailList spamMails;
    ...
    ...
    
    public void classify(MailDetails mail, 
                            boolean addToMailList)
    {
        ...
        switch (mail.getSenderStatus())
        {
            case MailDetails.SENDER_KNOWN :
                ...
                possibleSpamMails.addMail(mail);
                break;
            case MailDetails.SENDER_UNKNOWN :
                ...
                possibleSpamMails.addMail(mail);
                break;
            case MailDetails.SENDER_SPAMMER :
                ...
                spamMails.addMail(mail);
                break;
        }
        ...
    }
} 

public class CustomTableModel extends 
            AbstractTableModel implements MailList
{
    private Callback onMailAdd;
    ...
    public void addMail(MailDetails mailDetails)
    {
        ...
        fireTableRowsInserted(index, index);

        onMailAdd.call();
    }
}

I generally use different Callbacks for different outcomes such as onSuccess and onFailure. The Callback interface can be refactored to send a List of Exceptions or messages as a parameter in the call(...) method. If the parameter is null then it can be assumed to have succeeded.

The code below shows how the main Mails class has separate Callbacks for each category of mails. They are invoked by their respective CustomTableModels as seen in the code above, whenever a mail gets added or deleted.

public class Mails
{
    private final Callback onMailAdd = 
    new Callback()
    {
        Runnable runnable = new Runnable()
        {
            public void run()
            {
                //Update Statistics here.
                ...
            }
        };

        public void call()
        {
            SwingUtilities.invokeLater(runnable);
        }
    };

    private final Callback onPossSpamMailAdd = 
    ...
    ...
    private final Callback onSpamMailAdd =
    ...
    ...
    
    protected void prepareMailTables(
                                  boolean refresh)
    {
       ...

       //Mails table
       mailsTableModel = 
                  new CustomTableModel(onMailAdd);
       ...
        
       //Hookup MailProcessor and CustomTableModel.
       //CustomTableModel implements MailList 
       //interface.
       //MailProcessor adds processed mail to the 
       //Model using only the interface.
       MailProcessor mailProcessor = 
                         Setup.getMailProcessor();
        ...
       mailProcessor.setMails(mailsTableModel);
       mailProcessor.setPossibleSpamMails(
                              possSpamTableModel);
       mailProcessor.setSpamMails(
                                  spamTableModel);
    }
}

Code Reuse

One of the fundamental benefits of OOP is code reuse.

While developing BlackMamba, I faced a situation where three different kinds of data had to be displayed/edited from the UI: address book, list of spammers, and list of Spam Subjects. All of them required a JList to display the list and on selecting any one of them the details had to be displayed for editing/deletion on the right-hand side with buttons to create, save, and delete the entry on the extreme right. Only the panel that displayed the details would change. This arrangement is shown in Figure 6.

Figure 6
Figure 6. Subclass of ListConfig Panel, where the portion highlighted in red is the variation.

To avoid rewriting the code three times, I made a single widget class that held all the common widgets and three different JPanel extensions to display the details. This new JPanel extension is added to a predetermined place in the ListConfig panel by the controller class. There is a saying, "Good programmers know what to write. Great ones know what to rewrite (and reuse)."

public class Configure
{
    public void init(MambaFrm mambaFrm)
    {
        configureDlg = new ConfigureDlg(mambaFrm, 
                                            true);
        configureDlg.setLocationRelativeTo(null);
        configureDlg.setDefaultCloseOperation(
               javax.swing.WindowConstants \
                            .DO_NOTHING_ON_CLOSE);
        configureDlg.addWindowListener(
        new java.awt.event.WindowAdapter()
        {
            public void windowClosing(
                   java.awt.event.WindowEvent evt)
            {
                dispose();
            }
        });

        prepareAddressesTab();
        prepareSpammersTab();
        prepareSubjectsTab();

        configureDlg.pack();
        configureDlg.show();
    }
   
    protected void prepareAddressesTab()
    {
        ListConfig addressListConfig = 
                          new AddressListConfig();
        ...
        addressListConfig.init(
                configureDlg.addressListConfigPnl,    
                addressesListModel);
    }

    protected void prepareSpammersTab()
    {
        ListConfig spammerListConfig = 
                          new SpammerListConfig();
        ...
        spammerListConfig.init(
                configureDlg.spammerListConfigPnl, 
                spammerListModel);
    }

    protected void prepareSubjectsTab()
    {
        ListConfig subjectsListConfig = 
                         new SubjectsListConfig();
        ...
        subjectsListConfig.init(
                configureDlg.subjectListConfigPnl, 
                subjectsListModel);
    }  
}

Because blackmamba.ui.view.ListConfig panel is used for blackmamba.ui.view.AddressListConfig, blackmamba.ui.view.SpammerListConfig, and blackmamba.ui.view.SubjectsListConfig, the common logic in the controller classes for these three can be refactored into blackmamba.ui.control.ListConfig class.

public abstract class ListConfig
{
    protected CustomListModel listModel;
    protected ListConfigPnl listConfigPnl;

    public void init(ListConfigPnl lstConfigPnl, 
                         CustomListModel lstModel)
    {
        listConfigPnl = lstConfigPnl;
        listModel = lstModel;

        listConfigPnl.aliasesLst.setModel(
                                       listModel);

        ListSelectionListener selectionListener = 
        new ListSelectionListener()
        {
            public void valueChanged(
                             ListSelectionEvent e)
            {
                ...
            }
        };

        listConfigPnl.aliasesLst \
                 .addListSelectionListener( \
                               selectionListener);

        //Setup Buttons
        listConfigPnl.newBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                newBtnClicked();
            }
        });

        //Save
        listConfigPnl.saveBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                Object newObj = saveBtnClicked();
                ...
            }
        });

        //Delete
        listConfigPnl.deleteBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                deleteBtnClicked();
                ...
            }
        });

        //Close
        listConfigPnl.closeBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                closeBtnClicked();
            }
        });
    }

    protected abstract void closeBtnClicked();

    protected abstract void deleteBtnClicked();

    /**
     * @return null to cancel Save. Or, the Object
     * to add.
     */
    protected abstract Object saveBtnClicked();

    protected abstract void newBtnClicked();

    protected abstract void listSelected(
                                       int index);
} 

class AddressListConfig extends ListConfig
{
    ...
    ...
}

Model

BlackMamba uses JTables to display the mails downloaded from the server. The number of rows can change dynamically. I did not use the javax.swing.table.DefaultTableModel because it uses a Vector of Vectors. Since Vectors are synchronized, the performance degrades if there are a lot of rows. And because the DefaultTableModel expects the column data to be separate objects, the granularity becomes too fine and unmanageable. A better design is to have a custom TableModel implementation: blackmamba.helpers.ui.CustomTableModel which uses the java.util.List interface as the backing datastore. The List elements or rows are instances of MailDetails. Also, by using the List interface, the implementation can be any one of ArrayList, LinkedList, or Vector. Since the mails are frequently added and deleted, LinkedList is an ideal choice of data-structure.

public class CustomTableModel 
    extends AbstractTableModel implements MailList
{
    private List mails;
    ...
   
    //------- start AbstractTableModel methods ---
    public int getRowCount()
    {
        return mails.size();
    }

    public int getColumnCount()
    {
        return 5;
    }

    public Object getValueAt(int rowIndex, 
                                  int columnIndex)
    {
        Object obj = null;

        MailDetails mailDetails = 
                (MailDetails) mails.get(rowIndex);
        if (mailDetails == null)
        {
            return obj;
        }

        switch (columnIndex)
        {
            case 0 :
                obj = getSmileyValue(mailDetails);
                break;

            case 1 :
                obj = new Boolean(
                 mailDetails.isMarkedForDelete());
                break;

            case 2 :
                obj = getSenderValue(mailDetails);
                break;

            case 3 :
                obj = getSizeValue(mailDetails);
                break;

            case 4 :
               obj = getSubjectValue(mailDetails);
               break;

            default :
                break;
        }

        return obj;
    }
   ...
    public String getColumnName(int column)
    {
        String ret = null;

        switch (column)
        {
            case 0 :
                ret = " ";
                break;
            case 1 :
                ret = "[Delete]";
                break;
            case 2 :
                ret = "From";
                break;
            case 3 :
                ret = "Size";
                break;
            case 4 :
                ret = "Subject";
                break;
            default :
                break;
        }

        return ret;
    }

    public boolean isCellEditable(int rowIndex, 
                                  int columnIndex)
    {
        boolean ret = false;

        switch (columnIndex)
        {
            case 1 :
                ret = true;
                break;
            default :
                break;
        }

        return ret;
    }

    public Class getColumnClass(int columnIndex)
    {
        Class clazz = String.class;

        switch (columnIndex)
        {
            case 0 :
                clazz = Smiley.class;
                break;
            case 1 :
                clazz = Boolean.class;
                break;
            default :
                break;
        }

        return clazz;
    }

    ...
    ...
    public void setValueAt(Object aValue, 
                    int rowIndex, int columnIndex)
    {
        if (columnIndex == 1)
        {
            MailDetails mailDetails = 
                (MailDetails) mails.get(rowIndex);
            Boolean b = (Boolean) aValue;
            mailDetails.setMarkedForDelete(
                                b.booleanValue());

            fireTableRowsUpdated(rowIndex, 
                                        rowIndex);
        }
    }
    //------- end AbstractTableModel methods -----
    
    public Object[] getPrototypeValues()
    {
        return new Object[] {
            Smiley.SMILEY_UGLY,
            Boolean.TRUE,
            "XXXXXXXXXXXXXXXXXXXXXXXXXX",
            "XXXXX",
            "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" };
    }
   
    //------- start MailList methods -------------
    public void setMails(List list)
    {
        mails = list;
        fireTableDataChanged();
    }
    
    public void addMail(MailDetails mailDetails)
    {
        mails.add(mailDetails);
        int index = mails.indexOf(mailDetails);
        fireTableRowsInserted(index, index);

        onMailAdd.call();
    }

    public void addAll(List list)
    {
        mails.add(list);
        fireTableDataChanged();
    }

    public void update(MailDetails mailDetails)
    {
        int index = mails.indexOf(mailDetails);
        fireTableRowsUpdated(index, index);
    }

   public void removeMail(MailDetails mailDetails)
   {
        int index = mails.indexOf(mailDetails);
        mails.remove(mailDetails);
        fireTableRowsDeleted(index, index);
   }

    public void removeAll()
    {
        mails.clear();
        fireTableDataChanged();
    }
    //------- end MailList methods ---------------
}

Only the second column is editable. Most of the columns render the data as Strings. The second editable column renders the Boolean data as a JCheckbox. The blackmamba.ui.control.mail.Mails class is responsible for creating, maintaining, and finally disposing of the tables. It rigs up the default renderer for the first two columns, which do not use the default renderers. The default column sizes are not very pleasing to the eye. So it also resizes the columns to make them look better by using prototype values for each column.

public class Mails
{
...
protected void customizeTable(JTable table, 
                           CustomTableModel model)
{
    JCheckBox checkBox = new JCheckBox();
    checkBox.setHorizontalAlignment(
                           SwingConstants.CENTER);
    table.setDefaultEditor(Boolean.class, 
                 new DefaultCellEditor(checkBox));

    DefaultTableCellRenderer smileyRenderer = 
                   new DefaultTableCellRenderer();
    smileyRenderer.setHorizontalAlignment(
                           SwingConstants.CENTER);
    table.setDefaultRenderer(Smiley.class, 
                                  smileyRenderer);

    TableColumn column = null;
    Component comp = null;
    int headerWidth = 0;
    int cellWidth = 0;
    Object[] longValues = 
                       model.getPrototypeValues();
    int cols = model.getColumnCount();
    TableColumnModel columnModel = 
                           table.getColumnModel();

    for (int i = 0; i < cols; i++)
    {
        column = columnModel.getColumn(i);

        comp =
            table.getTableHeader() \
                .getDefaultRenderer() \
                .getTableCellRendererComponent(
                null,
                column.getHeaderValue(),
                false,
                false,
                0,
                0);
        headerWidth = comp.getPreferredSize() \
                                           .width;

        comp =
            table.getDefaultRenderer(
                  model.getColumnClass(i)) \
                  .getTableCellRendererComponent(
                table,
                longValues[i],
                false,
                false,
                0,
                i);
        cellWidth = comp.getPreferredSize().width;

       int min = Math.min(headerWidth, cellWidth);
       int max = Math.max(headerWidth, cellWidth);

        switch (i)
        {
            case 0 :
            case 1 :
            case 3 :
                column.setPreferredWidth(min);
                break;
            default :
                column.setPreferredWidth(max);
                break;
        }
    }
}
...
}

Actions

It is very common to have widgets like buttons, popup menus, and toolbar menus to have the same functionality. BlackMamba has toolbar menus and buttons on one screen for Login, Logout, and Configuration. Instead of having ActionListener code strewn across classes, it can be written once, using the javax.swing.Action class. This class can be used for both menu items and buttons. This way, not only will the behavior be consistent, but also the look and feel. Both the menu items and the buttons will have the same mnemonics, accelerator keys, and same disabled/enabled state.

In BlackMamba, when the user has not logged into the server, Login is enabled and Logout is disabled, and vice-versa when the user has logged in. The logic to choose the mail server, connect to it, fetch mails, and log out is spread across several control classes. There can be a central location from which these Actions can be accessed to avoid passing it across screens.

public class Actions
{
public static final AbstractAction loginAction = 
new AbstractAction()
{
    public void actionPerformed(ActionEvent e)
    {
     ...
     ...
    }
};

public static final AbstractAction logoutAction = 
new AbstractAction()
{
    public void actionPerformed(ActionEvent e)
    {
     ...
     ...
    }
};
}

public class Start
{
...
protected void prepareMenus()
{
    //Login
    ActionPropsSetter.setActionProps(
        Actions.loginAction, mambaFrm.loginMnuIt);

    mambaFrm.loginMnuIt.setAction(Actions.loginAction);
    mambaFrm.loginMnuIt.addActionListener(
    new ActionListener()
    {
        public void actionPerformed(ActionEvent ae)
        {
            login();
        }
    });

    //Logout
    ActionPropsSetter.setActionProps(
      Actions.logoutAction, mambaFrm.logoutMnuIt);

    mambaFrm.logoutMnuIt.setAction(Actions.logoutAction);
    mambaFrm.logoutMnuIt.addActionListener(
    new ActionListener()
    {
       public void actionPerformed(ActionEvent ae)
       {
            Runnable runnable = new Runnable()
            {
                public void run()
                {
                    logout();
                }
            };
            SwingUtilities.invokeLater(runnable);
       }
    });

Actions.logoutAction.setEnabled(false);
}
}

public class Mails
{
...
protected void prepareButtons()
{
   ...
       mailsPnl.logoutBtn.addActionListener(
       new ActionListener()
       {
        public void actionPerformed(ActionEvent e)
        {
            beforeLogout();
        }
       });

       mailsPnl.logoutBtn.setAction(
                            Actions.logoutAction);
}
...
protected void beforeLogout()
{
   ...
       Actions.loginAction.setEnabled(true);
       Actions.logoutAction.setEnabled(false);
   ...
}
}

Technically, the ActionListener code should have been inside Actions.loginAction and Actions.logoutAction. But I added these Action classes as an afterthought. I had to write a small utility method to copy the text, mnemonics, and accelerator keys from my menu items and buttons that I had already created in NetBeans to the Action classes. When you add an Action class to a menu item or button, it overwrites the widget's settings. So plan your code in advance to avoid such hacks.

public class ActionPropsSetter
{
    public static void setActionProps(
                Action action, JMenuItem menuItem)
    {
        action.putValue(Action.NAME, 
                              menuItem.getText());
        action.putValue(Action.MNEMONIC_KEY, 
             new Integer(menuItem.getMnemonic()));
        action.putValue(Action.ACCELERATOR_KEY, 
                       menuItem.getAccelerator());
    }
}

Setup

BlackMamba uses a lot of helper classes: text databases, POP3 mail protocol helpers, mail classifiers, mail processors, threadpool, common data-structures, etc. It would require a lot of spaghetti code to move these instances around the control classes. A Singleton is a good solution. But my intention was not to force only one instance of helper classes in the VM, but to have a central location like a registry where all the configuration settings and helper class instances could be retrieved. I could also have all my initialization code here, such as reading files, loading images, initializing the Swing look and feel, etc. The only Singleton here is the registry. Having just one single place from which all changes can be made also improves code clarity.

Another case against turning all the helper classes into Singletons is that it hinders the use of MockObjects for unit testing. For example if blackmamba.helpers.mail.MailHelper had to be replaced by a MockMailHelper that would just simulate a POP3 server's functions for unit testing, I would have to change all the classes that use MailHelper with MockMailHelper. Or I would have to send an instance of MockMailHelper in the static getInstance() method of MailHelper. Also, since the static getInstance() method on the Singleton is the only way to create and access the MailHelper, when a new sub-class has to be used instead of the original helper class for say, IMAP4 instead of POP3 this static method cannot be overridden to return IMAP4MailHelper.

To avoid all this, I write MailHelper like a regular class with a public constructor. My blackmamba.Setup class will have a public static reference to MailHelper. MockMailHelper can extend MailHelper and override all methods to simulate a POP3 server. A drawback of using a central registry instead of explicit Singletons is that there is nothing preventing the developer from creating multiple instances of the helper classes.

public class Setup
{
private static AccountsDatabase accountsDatabase;
...

private static LiteThreadPool liteThreadPool;
private static MailProcessor mailProcessor;
private static MailHelper mailHelper;

...

public static final boolean DEBUG = false;

public static final String DIR_NAME_RESOURCES = 
                                      "resources";
public static final String FILE_NAME_ACCOUNTS = 
                                 "AccountsDB.txt";
...

public static final int DEF_MAILS_SIZE = 
                                  15 * 100 * 1000;
...

public static final int FETCH_SIZE = 10;

private Setup()
{
}
...
public static void init() throws Exception
{
    initDatabases();

    //LiteThreadPool
    liteThreadPool = new LiteThreadPool();

    //MailProcessor
    mailProcessor = new MailProcessor();

    //MailHelper
    mailHelper = new MailHelper();
}

protected static void initDatabases() 
throws IOException
{
    String accountsDatabaseFile = new File(
    rootDir, 
    FILE_NAME_ACCOUNTS).getAbsolutePath();
    
    ...

    //AccountsDatabase
    accountsDatabase = new AccountsDatabase();
    accountsDatabase.setFileName(
                            accountsDatabaseFile);
    ...
    accountsDatabase.load();
    ...
    spammersDatabase.load();
    ...
    subjectsDatabase.load();
    ...
    sizesDatabase.load();
}

public static void initLF()
{
    ...
    UIManager.setLookAndFeel(
       UIManager.getSystemLookAndFeelClassName());
    ...
}

public static AccountsDatabase 
getAccountsDatabase()
{
    return accountsDatabase;
}

...
...

public static LiteThreadPool getLiteThreadPool()
{
    return liteThreadPool;
}

public static MailProcessor getMailProcessor()
{
    return mailProcessor;
}

public static MailHelper getMailHelper()
{
    return mailHelper;
}
...
}

Using a registry is not the ultimate solution. Figure 5 shows that the number of dependencies on Setup class is quite high. It will not scale well. It is susceptible to the same kind of problems that face the bloated controller class. There is a concept called Inversion of Control, which is really a geeky version of the Hollywood Principle: "Don't call us, we'll call you."

In this arrangement all important classes are turned into Components, which usually means implementing a few Lifecycle interfaces and registering these classes in a properties or XML file. These "Components" must not be confused with java.awt.Components. The Components here are managed by a lightweight container that reads the configuration/setup file, instantiates the Components, and supplies references to other Components and any configuration information through these Lifecycle methods.

A well-known example is the Java Servlet framework. All Servlets extend the HTTPServlet class. The Servlets are registered with the Web Server by filling up the web.xml file. Initialization parameters can be supplied in the web.xml file. The Servlets are created, initialized with environment variables and other start up parameters, used to service HTTP requests, and finally discarded by the web server. Servlets can invoke other Servlets in the web application by just forwarding requests to the URL on which the other Servlet is listening. Even the URL to be invoked can be parameterized using the web.xml file.

As you can see there is a considerable level of de-coupling between Components in this framework.

Summary

I hope by now you are able to fully appreciate the extent of careful planning that should go into developing an application. Notice that nowhere have I even mentioned the internal workings of BlackMamba. Things like how it fetches mails from the server; what kind of logic it uses to classify mails; or where the account information, list of blocked senders, address book, etc. are maintained. Just describing the aspects common to every application itself required a whole article.

The important points to remember are:

Resources

Ashwin Jayaprakash is a software engineer at BEA Systems R & D Centre, Bangalore, using Java/J2EE to develop WebLogic Integration products.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.