Building a Simple RSS Client in Pivot
Join the DZone community and get the full member experience.
Join For FreeA 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.
Opinions expressed by DZone contributors are their own.
Comments