Home > Articles > Programming > Java

Java Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

RSS Java GUI

Last updated Mar 14, 2003.

Reading an RSS feed as a text stream is the essence of what the Informa library gives us, but it is not very usable. (I'll cover database support in a forthcoming article.) Therefore, I thought we would take a little diversion into building a Swing GUI to manage RSS feeds. In this first iteration, we'll set the stage with a GUI framework driven by an XML configuration file. In the next iteration, we'll add support for more flexibility and user configuration.

Design

We'll organize our RSS manager into categories that contain channels: a master list of channels, with the ability for the user to create categories and add channels to those categories. To manage a hierarchical organization, we use an XML file to store the configuration. The format of the XML is shown in listing 7.

Listing 7. jrss.xml configuration file

<jrss>
 <categories>
  <category name="J2EE">
    <channel name="InformIT :: Java Reference Guide" />
    <channel name="TSS" />
  </category>
  <category name="News">
    <channel name="Yahoo Top Stories" />
    <channel name="Yahoo World" />
    <channel name="Yahoo Politics" />
    <channel name="Yahoo Business" />
    <channel name="Yahoo Technology" />
    <channel name="Yahoo Entertainment" />
    <channel name="Yahoo Sports" />
  </category>
 </categories>
 <channels>
  <channel name="InformIT :: Java Reference Guide" url="http://www.informit.com/guides/guide_rss.asp?g=java" />
  <channel name="TSS" url="http://www.theserverside.com/rss/theserverside-rss2.xml" />
  <channel name="Yahoo Top Stories" url="http://rss.news.yahoo.com/rss/topstories" />
  <channel name="Yahoo World" url="http://rss.news.yahoo.com/rss/world" />
  <channel name="Yahoo Politics" url="http://rss.news.yahoo.com/rss/politics" />
  <channel name="Yahoo Business" url="http://rss.news.yahoo.com/rss/business" />
  <channel name="Yahoo Technology" url="http://rss.news.yahoo.com/rss/tech" />
  <channel name="Yahoo Entertainment" url="http://rss.news.yahoo.com/rss/entertainment" />
  <channel name="Yahoo Sports" url="http://rss.news.yahoo.com/rss/sports" />
 </channels>
</jrss> 

This XML document defines two types of data:

  • Channels: these channel feeds define a name to identify the feed, and a URL that points to the URL from which to download the feed.

  • Categories: a category is a user-defined grouping with a name of his choice. Each category can contain zero or more channels; each channel is referenced by name and the URL cross-referenced upon request.

For this example, I defined two categories, News and J2EE, and assigned various channels to them. The channel name is simply an identifier; the application connects to the feed to get the actual name at runtime.

Figure 42 shows an example of the Java RSS Reader running with this XML configuration file.

Figure 42Figure 42. Screenshot of the Java RSS Reader

To define the user interface, we'll employ the following components:

  • Multiple Document Interface: although Microsoft does not like MDI applications, it fits here. The main window maintains a single "RSS Feed Manager" and then displays as many article windows as the user requests.

  • RSS Feed Manager: the RSS Feed Manager is driven by the aforementioned XML file and contains a JTree displaying categories and their channels. When a channel is selected, a JTable displays the title and date of the items in the channel. Double-clicking on an item opens another window that displays the contents of the item.

  • Content Window: a content window displays the header information for an item, as well as the contents of its target link.

From a Swing perspective, by the time we are done we will have touched upon the following components:

  • Using trees with JTree

  • Using tables with JTable

  • Building MDI applications using JDesktopPane and JInternalFrame

  • Building menus with JMenu, JMenuItem, etc.

  • Build a toolbar with JToolbar

  • Using actions to configure like behaviors

So let's get started.

RSS Feed Manager

The RSS Feed Manager is the heart of the Java RSS Reader (JRSS). From Figure 42 you can see that it is divided in half by a JSplitPane, with the left half holding a JTree and the right half holding a JTable. The container class that holds RSS Feed Manager is the JRSS class, which at this point is simply a JFrame containing a JDesktopPane and the RSSManagerFrame. Listing 8 shows the code for the JRSS class.

Listing 8. JRSS.java

package com.javasrc.rss.gui;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class JRSS extends JFrame
{
  private JDesktopPane desktop = new JDesktopPane();
  private RSSManagerFrame rssManager = new RSSManagerFrame();
  public JRSS()
  {
    super( "Java RSS Reader (JRSS)" );

    // Setup the contents of the frame
    desktop.add( rssManager );
    this.getContentPane().add( desktop, BorderLayout.CENTER );

    // Setup the screen: app size, center, make visible, add window closer
    this.setSize( 800, 600 );
    Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
    this.setLocation( d.width / 2 - 400, d.height / 2 - 300 );
    this.setVisible( true );
    this.addWindowListener( new WindowAdapter() {
        public void windowClosing(WindowEvent e)
        {
          System.exit( 0 );
        }
        } );
  }

  public static void main( String[] args )
  {
    JRSS jrss = new JRSS();
  }
}

Multiple Document Interface (MDI) applications are created by defining a JDesktopPane and adding it to a JFrame or other container. Usually, we add it to the CENTER of a BorderLayout so that it takes over the entire application space. Once you have a JDesktopPane, it acts as a parent for JInternalFrame instances (or derivatives). With a JDesktopPane in hand, all you need to do is create a JInternalFrame derivative and call the JDesktopPane's add() method. And there you have it: MDI in a nutshell.

So all of the fun takes place in the RSSManagerFrame class. However, the core logic of this application and how it integrates with the Informa RSS library occurs in one type of tree node (FeedNode) and in the RSSItemTableModel.

But let's start by looking through the RSSManagerFrame class shown in listing 9.

Listing 9. RSSManagerFrame.java

package com.javasrc.rss.gui;

// Import the Java classes
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.util.*;

// Import JDOM
import org.jdom.*;
import org.jdom.input.*;
import org.jdom.output.*;

// Import the Informa libraries
import de.nava.informa.core.*;

// Import our classes
import com.javasrc.rss.gui.tree.*;
import com.javasrc.rss.gui.table.*;
import com.javasrc.rss.util.*;

/**
 * Represents the internal frame that holds the tree of categorized channels and
 * facilitates the presentation of items in the channels
 */
public class RSSManagerFrame extends JInternalFrame implements TreeSelectionListener
{
  private JTree tree;
  private DefaultTreeModel treeModel;
  private DefaultMutableTreeNode root = new DefaultMutableTreeNode( "RSS Feeds" );
  private RSSItemTableModel tableModel = new RSSItemTableModel();
  private JTable table = new JTable( tableModel );
  private Map channelMap = new TreeMap();
  private Map catMap = new TreeMap();

  public RSSManagerFrame()
  {
    super( "RSS Feed Manager", true, false, true, true );

    // Build our tree
    treeModel = new DefaultTreeModel( root );
    tree = new JTree( treeModel );
    tree.addTreeSelectionListener( this );

    // Load our configuration
    try
    {
      SAXBuilder builder = new SAXBuilder();
      Document doc = builder.build( "jrss.xml" );
      Element root = doc.getRootElement();

      // Read our channels
      java.util.List channelList = root.getChild( "channels" ).getChildren( "channel" );
      for( Iterator i=channelList.iterator(); i.hasNext(); )
      {
        Element channelElement = ( Element )i.next();
        String name = channelElement.getAttributeValue( "name" );
        String url = channelElement.getAttributeValue( "url" );
        ChannelIF channel = RSSUtils.getChannel( url );
        this.channelMap.put( name, channel );
      }

      // Read our categories
      java.util.List catList = root.getChild( "categories" ).getChildren( "category" );
      for( Iterator i=catList.iterator(); i.hasNext(); )
      {
        Element category = ( Element )i.next();
        String catName = category.getAttributeValue( "name" );
        this.addCategory( catName );
        ArrayList catChannelList = new ArrayList();
        this.catMap.put( catName, catChannelList );
        java.util.List channels = category.getChildren( "channel" );
        for( Iterator j=channels.iterator(); j.hasNext(); )
        {
          Element channel = ( Element )j.next();
          String channelName = channel.getAttributeValue( "name" );
          catChannelList.add( channelName );
          this.addFeedToCategory( catName, ( ChannelIF )this.channelMap.get( channelName ) );
        }
      }
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }

    // Build our frame
    this.getContentPane().add( new JSplitPane( JSplitPane.HORIZONTAL_SPLIT, 
                          new JScrollPane( tree ), 
                          new JScrollPane( table ) ) );

    // Show the frame
    this.setVisible( true );
    this.setSize( 600, 400 );
    this.setLocation( 1, 1 );
  } 

  public void addCategory( String categoryName )
  {
    this.root.add( new CategoryNode( categoryName ) );
    treeModel.nodeStructureChanged( this.root );
  }

  public void addFeedToCategory( String categoryName, ChannelIF feed )
  {
      Enumeration e = this.root.children();
      while( e.hasMoreElements() )
      {
        CategoryNode categoryNode = ( CategoryNode )e.nextElement();
        if( categoryNode.getName().equalsIgnoreCase( categoryName ) )
        {
          categoryNode.add( new FeedNode( feed ) );
          treeModel.nodeStructureChanged( categoryNode );
          return;
        }
      }

      // Whoops, not here, add the category too..
      CategoryNode categoryNode = new CategoryNode( categoryName );
      categoryNode.add( new FeedNode( feed ) );
      this.root.add( categoryNode );
      treeModel.nodeStructureChanged( this.root );
  }

  /**
   * Tree Selection Event: called when the user clicks on a node in the JTree
   */
  public void valueChanged( TreeSelectionEvent e ) 
  {
    TreeNode selectedNode = ( TreeNode )e.getNewLeadSelectionPath().getLastPathComponent();
    if( selectedNode instanceof FeedNode )
    {
      ChannelIF channel = ( ChannelIF )( ( ( DefaultMutableTreeNode )selectedNode ).getUserObject() );
      System.out.println( "Channel selected: " + channel.getTitle() );
      this.tableModel.setChannel( channel );
      // Continue...
    }
  }
}

The first thing to note from listing 9 is that the RSSManagerFrame extends JInternalFrame, making it a candidate to participate in an MDI application owned by a JDesktopPane. The call to its super constructor reads as follows:

super( "RSS Feed Manager", true, false, true, true );

This follows the longest JInternalFrame constructor:

JInternalFrame(String title, boolean resizable, boolean closable, boolean maximizable, boolean iconifiable)

So we are saying that we want the RSS Feed Manager to be resizable, maximizable, and iconifiable, but not closable.

Typically trees are built through an exploratory process, and thus the logic for trees can be found in its nodes. In this case, we defined a Category node and a FeedNode node. For this example, the FeedNode is the most interesting (in subsequent articles, we will add popup menus on the CategoryNode, which is why I make the differentiation now). The JTree is constructed containing a root node that is of type DefaultMutableTreeNode. The DefaultMutableTreeNode class is a tree node that we usually use for a base class or simply for placeholder nodes (see the Java Reference Guide on JTree for more information)

To populate the tree, we read in the configuration file found in listing 7 using JDOM. JDOM is an open source project that provides a very Java-centric approach to reading XML files. If it were not for JDOM, I would not use XML unless I had to! We first read the configuration file and extract all of the channels – we maintain the channel information in a map of the channel name to its URL. Next, we read through all of the categories and their associated channels and build CategoryNodes and FeedNodes. We pass each FeedNode a ChannelIF instance (from earlier in this series), but instead of building one manually, I added a helper class: RSSUtils, which has a static method getChannel(String url), to do the following:

URL url = new URL( urlString );
ChannelIF channel = FeedParser.parse( new ChannelBuilder(), url );
return channel;

Before leaving the RSSManagerFrame class, observe that it implements the TreeSelectionListener interface. When a FeedNode is selected, it extracts the ChannelIF that it was constructed with and calls the RSSItemTableModel's setChannel() method. This causes the table to connect to the channel and read all of the items to display a summary of.

Listing 10 shows the code for the FeedNode class.

Listing 10. FeedNode.java

package com.javasrc.rss.gui.tree;

import javax.swing.tree.*;

// Import the Informa libraries
import de.nava.informa.core.*;

public class FeedNode extends DefaultMutableTreeNode
{
  public FeedNode( ChannelIF channel )
  {
    this.setUserObject( channel );
  }

  public boolean getAllowsChildren()
  {
    return false;
  }
  
  public boolean isLeaf()
  {
    return true;
  }
  
  public String toString()
  {
    ChannelIF channel = ( ChannelIF )this.getUserObject();
    return channel.getTitle();
  }
}

The core observations to make about the FeedNode are:

  1. It stores the ChannelIF instance in the tree node's user object. This is a common way to store information that will be passed between your application and the tree node.

  2. To display the name of the node, it obtains the ChannelIF from the user object part of the tree node.

  3. It calls the ChannelIF's getTitle() method; this is why our XML naming convention works as a cross reference.

Listing 11 shows the code for the RSSItemTableModel, the model that drive's the application's JTable.

Listing 11. RSSItemTableModel.java

package com.javasrc.rss.gui.table;

// Import the Java classes
import javax.swing.*;
import javax.swing.table.*;
import java.util.*;

// Import the Informa Libraries
import de.nava.informa.core.*;

/**
 * Table Model representing a list of RSS Items
 */
public class RSSItemTableModel extends AbstractTableModel
{
  private ArrayList list = new ArrayList();

  public RSSItemTableModel()
  {
  }

  public void setChannel( ChannelIF channel )
  {
    // Remove the old items
    int size = list.size();
    list.clear();
    this.fireTableRowsDeleted( 0, size );

    // Add the new items
    list.addAll( channel.getItems() );
    this.fireTableRowsUpdated( 0, list.size() - 1 );
  }

  public void addItem( ItemIF item )
  {
    this.list.add( item );
    this.fireTableRowsInserted( list.size() - 1, list.size() - 1 );
  }

  public boolean isCellEditable(int rowIndex, int columnIndex)
  {
    return false;
  }
  
  public String getColumnName(int column)
  {
    switch( column )
    {
    case 0: return "Title";
    case 1: return "Date";
    case 2: return "Link";
    default: return "Unknown";
    }
  }
  
  public Class getColumnClass(int columnIndex)
  {
    return new String().getClass();
  }
  
  public int getColumnCount()
  {
    return 3;
  }
  
  public int getRowCount()
  {
    return this.list.size();
  }
  
  public Object getValueAt(int rowIndex, int columnIndex)
  {
    ItemIF item = ( ItemIF )this.list.get( rowIndex );
    switch( columnIndex )
    {
    case 0: return item.getTitle();
    case 1: return item.getDate();
    case 2: return item.getLink();
    default: return "Unknown";
    }
  }
}

The RSSItemTableModel maintains an ArrayList of ItemIF objects; they are initialized in the setChannel() method. We simply borrow the list of ItemIF instances from the ChannelIF's getItems() method. We have to do this because we need a count and a random index into the list. The remaining code in the RSSItemTableModel is pretty straightforward: when an item is requested we look up the item in our ArrayList and call one of its helper methods (such as getTitle(), getDate(), and getLink()). See the Java Reference Guide on JTable for more information.

Summary

Thus far, we looked at the basics of reading RSS feeds. We wrote code against the Informa abstraction layer that allowed us to connect to any RSS feed, download its contents, and peruse its channels and nested items. Next, we started building a graphical user interface around the Informa.

Next week, we will continue building the GUI; in subsequent articles we will add our own persistence to our RSS feeds. Finally, we will look at connecting this RSS reader application to a database, using the built in support in Informa for the Hibernate O/R Mapping tool.