Greg has posted 18 posts at DZone. You can read more from them at their website. View Full User Profile

Building a Simple RSS Client in Pivot

04.23.2009
| 7399 views |
  • submit to reddit

A few months ago, this article by Andrew Trice inspired me to write a demo application to see how well Pivot would handle very large tabular data sets of up to one million rows (the results are here). Mr. Trice has again inspired me, this time to see how easy it might be to to write a Pivot application that consumes an RSS feed and presents the data in a list view using a custom item renderer. This article describes the results.

The Demo Application

The following image shows a screen shot of the demo application, which retrieves RSS feed data from Javalobby. A live example is available here (it requires Java 6 Update 10 or later; see notes at bottom of page). The application contains a single ListView component in a ScrollPane that is used to present the news data. A custom list item renderer is used to display the item's title, categories, and submitter.

The WTKX source for the demo's UI is as follows:

<Border styles="{padding:0, color:10}"
xmlns:wtkx="http://incubator.apache.org/pivot/wtkx/1.1"
xmlns:effects="pivot.wtk.effects"
xmlns:rss="pivot.demos.rss"
xmlns="pivot.wtk">
<content>
<CardPane wtkx:id="cardPane" selectedIndex="0">
<Label wtkx:id="statusLabel" text="Loading..."
styles="{horizontalAlignment:'center', verticalAlignment:'center'}"/>
<ScrollPane horizontalScrollBarPolicy="fill">

<view>
<ListView wtkx:id="feedListView"/>
</view>
</ScrollPane>
</CardPane>
</content>

</Border>

It's pretty straightforward: a root Border component contains a CardPane containing two sub-panes. The first is a status label that is used to provide feedback to the user while the feed is being loaded. The second is a ScrollPane containing the actual list view used to present the feed data.

The Java source is a bit more involved. It is broken into two files: one contains the main application class, and the other contains an adapter class that is used to wrap an XML node list such that it can be used as the data model for the list view. The application class contains several inner classes that are used to facilitate loading and presenting the data, as well as opening the articles in an external browser window when the user double-clicks on the list.

The Main Application Class

The application's constructor is shown below:

public RSSFeedDemo() {
// Create an XPath instance
xpath = XPathFactory.newInstance().newXPath();

// Set the namespace resolver
xpath.setNamespaceContext(new NamespaceContext() {
public String getNamespaceURI(String prefix) {
String namespaceURI;
if (prefix.equals("dz")) {
namespaceURI = "http://www.developerzone.com/modules/dz/1.0";
} else {
namespaceURI = XMLConstants.NULL_NS_URI;
}

return namespaceURI;
}

public String getPrefix(String uri) {
throw new UnsupportedOperationException();
}

public Iterator getPrefixes(String uri) {
throw new UnsupportedOperationException();
}
});
}

Using the newInstance() factory method, it obtains an instance of javax.xml.xpath.XPath that is later used to retrieve element values from the returned data. A namespace resolver is then set on the XPath, allowing it to map the "dz" prefix to the "http://www.developerzone.com/modules/dz/1.0" namespace URI.

The startup() method is defined as follows:

public void startup(Display display, Dictionary properties)
throws Exception {
WTKXSerializer wtkxSerializer = new WTKXSerializer();
window = new Window((Component)wtkxSerializer.readObject(getClass().getResource("rss_feed_demo.wtkx")));

feedListView = (ListView)wtkxSerializer.getObjectByName("feedListView");
feedListView.setItemRenderer(new RSSItemRenderer());
feedListView.getComponentMouseButtonListeners().add(new FeedViewMouseButtonHandler());

final CardPane cardPane = (CardPane)wtkxSerializer.getObjectByName("cardPane");
final Label statusLabel = (Label)wtkxSerializer.getObjectByName("statusLabel");

LoadFeedTask loadFeedTask = new LoadFeedTask();
loadFeedTask.execute(new TaskListener() {
public void taskExecuted(Task task) {
feedListView.setListData(new NodeListAdapter(task.getResult()));
cardPane.setSelectedIndex(1);
}

public void executeFailed(Task task) {
statusLabel.setText(task.getFault().toString());
}
});

window.setMaximized(true);
window.open(display);
}

It loads the UI from the WTKX file, sets it as the content of a decorationless window, and obtains references to some of the components defined in the file. It assigns an instance of RSSItemRenderer as the item renderer for the list view and attaches an instance of FeedViewMouseButtonHandler as a mouse button listener on the list view (these classes are discussed in more detail below). It then creates an instance of LoadFeedTask, executes the task, and opens the window.

LoadFeedTask

LoadFeedTask is a private inner class that is used to load the feed data on a background thread so the UI doesn't block while it is being loaded. It is defined as follows:

private class LoadFeedTask extends IOTask<NodeList> {
public NodeList execute() throws TaskExecutionException {
NodeList itemNodeList = null;

DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder;
try {
documentBuilder = documentBuilderFactory.newDocumentBuilder();
} catch(ParserConfigurationException exception) {
throw new TaskExecutionException(exception);
}

Document document;
try {
document = documentBuilder.parse(FEED_URI);
} catch(IOException exception) {
throw new TaskExecutionException(exception);
} catch(SAXException exception) {
throw new TaskExecutionException(exception);
}

try {
itemNodeList = (NodeList)xpath.evaluate("/rss/channel/item",
document, XPathConstants.NODESET);
} catch(XPathExpressionException exception) {
// No-op
}

return itemNodeList;
}
}

The execute() method loads an XML document from the feed URI and then uses the XPath object to obtain a node list containing the item elements, which it returns. Upon successful execution, the taskExecuted() method of the task listener is called. This method wraps the returned node list in an instance of NodeListAdapter, which is used to adapt the contents of the NodeList for consumption by the ListView (the source code for this class is available here).

If the load fails, the resulting execption is displayed in the status label.

RSSItemRenderer

The RSSItemRenderer class prepares the feed data for presentation in the list view. It implements the ListView.ItemRenderer interface and extends a vertical FlowPane, which it uses to arrange Label instances containing each article's title, categories, and submitter (note that, while this implementation creates the flow pane's layout programmatically, it could also have been defined in WTKX):

private class RSSItemRenderer extends FlowPane implements ListView.ItemRenderer {
private Label titleLabel = new Label();
private Label categoriesHeadingLabel = new Label("subject:");
private Label categoriesLabel = new Label();
private Label submitterHeadingLabel = new Label("submitter:");
private Label submitterLabel = new Label();

public RSSItemRenderer() {
super(Orientation.VERTICAL);

getStyles().put("padding", new Insets(2, 2, 8, 2));

add(titleLabel);

FlowPane categoriesFlowPane = new FlowPane();
add(categoriesFlowPane);

categoriesFlowPane.add(categoriesHeadingLabel);
categoriesFlowPane.add(categoriesLabel);

FlowPane submitterFlowPane = new FlowPane();
add(submitterFlowPane);

submitterFlowPane.add(submitterHeadingLabel);
submitterFlowPane.add(submitterLabel);
}

...

The render method obtains the font and color styles from the component and applies them to its internal labels. It then sets the contents of the labels by extracting the element content from the current node.

    ...

public void render(Object item, ListView listView, boolean selected,
boolean checked, boolean highlighted, boolean disabled) {
// Render styles
Font labelFont = (Font)listView.getStyles().get("font");
Font largeFont = labelFont.deriveFont(Font.BOLD, 14);
titleLabel.getStyles().put("font", largeFont);
categoriesLabel.getStyles().put("font", labelFont);
submitterLabel.getStyles().put("font", labelFont);

Color color = null;
if (listView.isEnabled() && !disabled) {
if (selected) {
if (listView.isFocused()) {
color = (Color)listView.getStyles().get("selectionColor");
} else {
color = (Color)listView.getStyles().get("inactiveSelectionColor");
}
} else {
color = (Color)listView.getStyles().get("color");
}
} else {
color = (Color)listView.getStyles().get("disabledColor");
}

if (color instanceof Color) {
titleLabel.getStyles().put("color", color);
categoriesHeadingLabel.getStyles().put("color", color);
categoriesLabel.getStyles().put("color", color);
submitterHeadingLabel.getStyles().put("color", color);
submitterLabel.getStyles().put("color", color);
}

// Render data
if (item != null) {
Element itemElement = (Element)item;

try {
String title = (String)xpath.evaluate("title", itemElement, XPathConstants.STRING);
titleLabel.setText(title);

String categories = "";
NodeList categoryNodeList = (NodeList)xpath.evaluate("category", itemElement,
XPathConstants.NODESET);
for (int j = 0; j < categoryNodeList.getLength(); j++) {
Element categoryElement = (Element)categoryNodeList.item(j);
String category = categoryElement.getTextContent();
if (j > 0) {
categories += ", ";
}

categories += category;
}

categoriesLabel.setText(categories);

String submitter = (String)xpath.evaluate("dz:submitter/dz:username", itemElement,
XPathConstants.STRING);
submitterLabel.setText(submitter);
} catch(XPathExpressionException exception) {
System.err.println(exception);
}
}
}
}

FeedViewMouseButtonHandler

The FeedViewMouseButtonHandler class handles double-clicks on the list view. Clicking the list view once stores the index that was clicked (in case the user moves the mouse before a double-click occurs), and the selected item is opened on the double-click:

private class FeedViewMouseButtonHandler extends ComponentMouseButtonListener.Adapter {
private int index = -1;

@Override
public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
if (count == 1) {
index = feedListView.getItemAt(y);
} else if (count == 2
&& feedListView.getItemAt(y) == index) {
Element itemElement = (Element)feedListView.getListData().get(index);

try {
String link = (String)xpath.evaluate("link", itemElement, XPathConstants.STRING);
BrowserApplicationContext.open(new URL(link));
} catch(XPathExpressionException exception) {
System.err.print(exception);
} catch(MalformedURLException exception) {
System.err.print(exception);
}
}

return false;
}
};

Summary

So, building an RSS reader in Pivot was fairly straightforward. The XPath APIs included in the Java platform are very efficient for retrieving and parsing the feed data, and Pivot makes it easy to wrap the returned XML data in a list model and customize it's presentation in the list view.

However, one important lesson learned is that that Java's XML factory methods don't play nicely with applets. They rely on the JAR service provider architecture and will repeatedly look for service descriptor files on the applet's classpath, resulting in many unnecessary requests to the web server. The only way to prevent this is to set the codebase_lookup applet parameter to false:

<param name="codebase_lookup" value="false">

This works, but it is an ugly workaround - there are certainly valid use cases for using both codebase lookup and XML parsing in an applet; for example, an applet that parses XML that is dynamically generated from a servlet on the applet's classpath. Hopefully Sun (or Oracle) will address this issue in a future JVM update.

Also note that this demo takes advantage of Java 6 Update 10's new support for crossdomain.xml files. This handy feature allows an unsigned applet to make a network connection to a server outside of its origin, provided it was loaded from an approved URL. See this article for more information.

The full source code for the demo is available here. For more information on Apache Pivot, visit http://incubator.apache.org/pivot.

Published at DZone with permission of its author, Greg Brown.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Dominique De Vito replied on Tue, 2009/04/28 - 3:05am

As I am interested into Swing 2.0 examples, I like this post. And while I like too RSS Java applications, I am happy you have choosen that kind of example.

Take a look at those instructions:

WTKXSerializer wtkxSerializer = ..
window = ...
feedListView = (ListView)  wtkxSerializer.getObjectByName(...);
final CardPane cardPane = (CardPane) wtkxSerializer.getObjectByName(...);
final Label statusLabel = (Label) wtkxSerializer.getObjectByName(...);

Are you planning to skip those (annoying) instructions while adding binding through annotations ?

IMHO, it's doable, and worthwhile.

And while talking about getting shorter code, it is possible to apply style updates in a bulk mode in order to avoid the following instructions ?

titleLabel.getStyles().put("font", largeFont);
categoriesLabel.getStyles().put("font", largeFont);
submitterLabel.getStyles().put("font", largeFont);

Thanks.

 

Greg Brown replied on Tue, 2009/04/28 - 11:22am in response to: Dominique De Vito

Good questions - thanks for the feedback.

> Are you planning to skip those (annoying) instructions while adding binding through annotations?

Can you elaborate on this? What might such annotations look like, and how would they be used?

> And while talking about getting shorter code, it is possible to apply style updates in a bulk mode in order to avoid the following instructions ?

Do you have any thoughts on what kind of syntax/API might help streamline code like this?

Greg

Dominique De Vito replied on Tue, 2009/04/28 - 2:04pm in response to: Greg Brown

> Are you planning to skip those (annoying) instructions while adding binding through annotations?

Can you elaborate on this? What might such annotations look like, and how would they be used?

 

I have first thought about something like (with appropriate setter definitions) :

@WtkxResource("rss_feed_demo.wtkx")
private Window window = null;
   
@Bind("feedListView")
private ListView feedListView = null;

@Bind("cardPane")
private CardPane cardPane = null;
   
@Bind("statusLabel")
private Label statusLabel = null;

I thought, if the 'Application' class lifecycle/method chain call enables it, annotations could be used to bind XML elements/Java fields, before the call of the startup() method.

But it's a first sketch. I am not fully happy with it.

I just feel annoyed needing to expose setter methods for all possible callers, while I just need them only here for initialization! So, I don't feel fully happy with the way constructors/DI works today.

> And while talking about getting shorter code, it is possible to apply style updates in a bulk mode in order to avoid the following instructions ?

Do you have any thoughts on what kind of syntax/API might help streamline code like this?

Why not: applyStyle(largeFont, titleLabel, categoriesLabel, submitterLabel);

with applyStyle() being defined as a static method into an helper class, using JDK 5 varargs ?

It could be even nicer if JDK 7 authorizes the following notation (if I remember correctly, Scala (or Groovy ?) authorizes this notation for static methods):

largeFont.applyStyle(titleLabel, categoriesLabel, submitterLabel);

So, 1 line for 3 lines in your example.

But for sure, it's a matter of taste ;-)

Dominique

http://www.jroller.com/dmdevito 

Greg Brown replied on Tue, 2009/04/28 - 4:16pm

I like where you are going with the annotations. I don't like the applyStyle() method quite as much, but, as you say, it is a matter of taste. :-)

Would you be willing to re-post these suggestions to the pivot-dev mailing list?

http://incubator.apache.org/pivot/lists.html

Eric Schwarzenbach replied on Tue, 2009/04/28 - 5:44pm

About the "repeatedly look for service descriptor files on the applet's classpath" problem. I assume this the JAXP finding-the-implementation-class behavior, right? Is there any way of setting a system property for an applet (short of client side configuration)? If there is, you can set javax.xml.parsers.DocumentBuilderFactory to "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl" if you want the parser impl from the jre / jdk or "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl" if you're using Xerces. If this property is set, it shouldn't attempt the discovery lookups.

You could also blow off the JAXP mechanism and use one of these implementation classes directly.

Cheers, Eric

Greg Brown replied on Wed, 2009/04/29 - 8:13am in response to: Eric Schwarzenbach

> I assume this the JAXP finding-the-implementation-class behavior, right?

Yes - the "codebase_lookup" applet param I mentioned in the article disables the lookup.

> Is there any way of setting a system property for an applet

I believe this is supported in Java 6 Update 10 and later.

So, there are a few workarounds, but it would be nice if we didn't need to use them.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.