Reading Newsfeeds in JavaFX with FeedRead
JavaFX 1.2 introduced many interesting APIs, including APIs for reading RSS and Atom newsfeeds. My recent article, Learn about JavaFX's APIs for Reading RSS and Atom Newsfeeds, introduces you to these APIs, and includes
behind-the-scenes material on how the FeedTask class polls newsfeeds.
This article presents FeedRead, a newsfeed reader that demonstrates how the RSS and Atom APIs simplify integrating newsfeed-reading code into a JavaFX application. You first explore this example's code, and then examine some oddities that arise when running the example.
Discovering FeedRead
I started to play with JavaFX's RSS and Atom APIs after encountering Mark Macumber's inspiring JavaFX and RSS blog post, and created an application for reading RSS and Atom newsfeeds: FeedRead. Figure 1 shows FeedRead's user interface.

Figure 1. FeedRead's colorful user interface lets you navigate through RSS and Atom newsfeeds.
FeedRead's user interface consists of a textbox for entering a newsfeed URL, a Go button for obtaining the feed, a feed item panel for displaying individual feed items, and a set of four navigation buttons. Each of the five buttons enables/disables itself as necessary.
The panel presents the item's title and a link to its Web page. When you run FeedRead as an applet, and you click the link, browsers such as Firefox reveal a new tab that presents this item. (Clicking the link achieves nothing when you run FeedRead as a standalone application.)
I used NetBeans IDE 6.5.1 with JavaFX 1.2 to create and test FeedRead. The same-named project consists of two source files
(Main.fx and FeedItemPanel.fx) and two PNG-based images (feedicon.png and
feedread.png). Check out Main.fx:
/*
* Main.fx
*/
package feedread;
import java.lang.Exception;
import javafx.data.feed.atom.AtomTask;
import javafx.data.feed.atom.Entry;
import javafx.data.feed.atom.Feed;
import javafx.data.feed.rss.Channel;
import javafx.data.feed.rss.Item;
import javafx.data.feed.rss.RssTask;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextBox;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Reflection;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Flow;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Alert;
import javafx.stage.Stage;
class FeedItem
{
var title: String;
var link: String
}
var items: FeedItem [];
var index: Integer = 0;
var feedImageURL: String;
var buttonGoRef: Button;
var textBoxURLRef: TextBox;
function go (url: String): Void
{
delete items;
index = 0;
buttonGoRef.disable = true;
buttonGoRef.focusTraversable = false;
RssTask
{
interval: 60s
location: url
onChannel: function (c: Channel)
{
feedImageURL = c.image.url
}
onItem: function (i: Item)
{
insert FeedItem
{
title: i.title
link: i.link
}
into items
}
onException: function (e: Exception)
{
def msg = e.getMessage ();
if (msg == "must use AtomTask for Atom feeds")
goAtom (url)
else
Alert.inform ("Error", e.getMessage ());
buttonGoRef.disable = false;
buttonGoRef.focusTraversable = true
}
onDone: function ()
{
buttonGoRef.disable = false;
buttonGoRef.focusTraversable = true;
}
}.update ()
}
function goAtom (url: String): Void
{
AtomTask
{
interval: 60s
location: url
onFeed: function (f: Feed)
{
feedImageURL = f.icon.uri
}
onEntry: function (e: Entry)
{
var title = e.title.text;
if (title.length () > 100)
title = "{title.substring (0, 100)}...";
insert FeedItem
{
title: title
link: e.links [0].href
}
into items
}
onException: function (e: Exception)
{
Alert.inform ("Error", e.getMessage ());
buttonGoRef.disable = false;
buttonGoRef.focusTraversable = true
}
onDone: function ()
{
buttonGoRef.disable = false;
buttonGoRef.focusTraversable = true
}
}.update ()
}
Stage
{
title: "FeedRead"
var sceneRef: Scene
scene: sceneRef = Scene
{
width: 550
height: 350
fill: LinearGradient
{
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops:
[
Stop { offset: 0.0 color: Color.NAVY },
Stop { offset: 0.5 color: Color.BLUEVIOLET },
Stop { offset: 1.0 color: Color.NAVY }
]
}
var flowRef: Flow
content: flowRef = Flow
{
vertical: true
vgap: 20
layoutX: bind (sceneRef.width-flowRef.layoutBounds.width)/2-
flowRef.layoutBounds.minX
layoutY: bind (sceneRef.height-flowRef.layoutBounds.height)/2-
flowRef.layoutBounds.minY
content:
[
ImageView
{
image: Image
{
url: "{__DIR__}res/feedread.png"
}
}
Flow
{
hgap: 20
var textBoxURLRef: TextBox
content:
[
Label
{
graphic: Text
{
content: "URL"
fill: Color.GOLD
font: Font
{
name: "Arial BOLD"
size: 14
}
effect: DropShadow
{
spread: 0.5
}
}
}
textBoxURLRef = TextBox
{
columns: 40
}
buttonGoRef = Button
{
text: "Go"
action: function (): Void
{
go (textBoxURLRef.text)
}
}
]
}
Group
{
content: FeedItemPanel
{
itemTitle: bind items [index].title
itemLink: bind items [index].link
feedImageURL: bind feedImageURL
effect: Reflection { fraction: 0.6 }
}
}
Flow
{
hgap: 20
content:
[
Button
{
text: "|<"
disable: bind if ((sizeof items == 0) or
(index == 0))
then true else false
focusTraversable: bind if ((sizeof items == 0) or
(index == 0))
then false else true
action: function (): Void
{
index = 0
}
}
Button
{
text: "<"
disable: bind if ((sizeof items == 0) or
(index == 0))
then true else false
focusTraversable: bind if ((sizeof items == 0) or
(index == 0))
then false else true
action: function (): Void
{
if (index != 0)
index--
}
}
Button
{
text: ">"
disable: bind if ((sizeof items == 0) or
(index == sizeof items-1))
then true else false
focusTraversable: bind if ((sizeof items == 0) or
(index == sizeof items-1))
then false else true
action: function (): Void
{
if (index != sizeof items-1)
index++
}
}
Button
{
text: ">|"
disable: bind if ((sizeof items == 0) or
(index == sizeof items-1))
then true else false
focusTraversable: bind if ((sizeof items == 0) or
(index == sizeof items-1))
then false else true
action: function (): Void
{
index = sizeof items-1
}
}
]
}
]
}
}
}
This code models a newsfeed as an items sequence of FeedItem instances (identifying each feed item's
title and link), an index into this sequence (identifying the current
FeedItem), and feedImageURL (a URL to the newsfeed's logo).
The model specification is followed by a go() function that's invoked when the user presses the
Go button. This function performs the following tasks:
-
Remove the previous newsfeed from the model:
delete items; index = 0; -
Disable the Go button so that it cannot be clicked:
buttonGoRef.disable = true; -
Prevent the user from shifting focus to this button via the keyboard:
buttonGoRef.focusTraversable = false; -
Assume that the newsfeed is in RSS format by instantiating
RssTask. -
Invoke
update()on theRssTaskinstance so that theonDone()function is invoked (if the newsfeed is in RSS format) -- it's important to re-enable the Go button when the newsfeed's items have been successfully obtained. -
From within
RssTask'sonException()function, invoke thegoAtom()function to instantiateAtomTaskand invoke itsupdate()function ifonException()'seargument'sgetMessage()method returns"must use AtomTask for Atom feeds". Although this test is brittle and could break in a future JavaFX version (if the message changes), it's easier than working withHttpRequestto obtain the newsfeed XML, and working withPullParserto parse enough of the feed to determine if it's Atom or RSS. WhenRssTask's orAtomTask'sonException()function is invoked, it's important to alert the user to the problem. This task is accomplished by invokingjavafx.stage.Alert'spublic static void inform(String title, String message)method to present a dialog displayinggetMessage()'s value. The Go button is then re-enabled.
The model's items sequence is populated in RssTask's onItem() and
AtomTask's onEntry() functions. Although I restrict the lengths of Atom feed item titles (which can
become quite lengthy), I haven't yet needed to do the same with RSS feed item titles.
Also, the model's feedImageURL variable is populated in RssTask's onChannel() function
via feedImageURL = c.image.url;, and in AtomTask's onFeed() function via
feedImageURL = f.icon.uri;.
At this point, the application's stage is created and its scene is laid out. The scene is organized within a
javafx.scene.layout.Flow instance, whose layoutX and layoutY variables are initialized
such that the scene is centered on the stage.
Perhaps you're wondering why I subtract flowRef.layoutBounds.minX from the rest of the expression in
layoutX: bind (sceneRef.width-flowRef.layoutBounds.width)/2-flowRef.layoutBounds.minX
and do something similar with layoutY.
Amy Fowler provides the rationale in her JavaFX1.2: Layout blog post. She first states that "to
establish a node's stable layout position, set layoutX/layoutY." Amy then goes on to state the
following:
Be aware that these variables define a translation on the node's coordinate space to adjust it from its current
layoutBounds.minX/minYlocation and are not final position values. This means you should use the following formula for positioning a node at x,y:node.layoutX = x - node.layoutBounds.minX node.layoutY = y - node.layoutBounds.minYOr, in the case of object literals, don't forget the
bind:def p = Polygon { layoutX: bind x - p.layoutBounds.minX layoutY: bind y - p.layoutBounds.minY ... }
The remaining scene code is fairly straightforward, but you might wonder why I wrap a FeedItemPanel instance in
a javafx.scene.Group instance. I do this to include this component's reflection in the group's layout bounds, so
the navigation buttons appear below the reflection.
The FeedItemPanel component is responsible for presenting the current FeedItem's (represented via
items [index]) title, and associating it with a link. This class's source code is presented below.
/*
* FeedItemPanel.fx
*/
package feedread;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Panel;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;
import javafx.stage.AppletStageExtension;
public class FeedItemPanel extends CustomNode
{
public var itemTitle: String;
public var itemLink: String;
public var feedImageURL: String on replace
{
if (feedImageURL == null or feedImageURL == "")
imageRef = Image { url: "{__DIR__}res/feedicon.png" }
else
imageRef = Image { url: feedImageURL }
}
var imageRef: Image;
override function create (): Node
{
Group
{
var hyperlinkRef: Hyperlink
var imageViewRef: ImageView
var rectangleRef: Rectangle
var titleTextRef: Text
content:
[
rectangleRef = Rectangle
{
width: 400
height: 80
arcWidth: 15
arcHeight: 15
stroke: Color.GOLD
strokeWidth: 3.0
fill: Color.WHITE
}
imageViewRef = ImageView
{
layoutX: bind (rectangleRef.layoutBounds.width-
imageViewRef.layoutBounds.width)/2-
imageViewRef.layoutBounds.minX
layoutY: bind (rectangleRef.layoutBounds.height-
imageViewRef.layoutBounds.height)/2-
imageViewRef.layoutBounds.minY
opacity: 0.3
fitWidth: bind if (imageRef.width > 380) then 380
else imageRef.width
fitHeight: bind if (imageRef.height > 60) then 60
else imageRef.height
preserveRatio: true
image: bind imageRef
}
titleTextRef = Text
{
layoutX: bind (rectangleRef.layoutBounds.width-
titleTextRef.layoutBounds.width)/2-
titleTextRef.layoutBounds.minX
layoutY: bind if (itemLink != null)
10-titleTextRef.layoutBounds.minY
else
(rectangleRef.layoutBounds.height-
titleTextRef.layoutBounds.height)/2-
titleTextRef.layoutBounds.minY
content: bind itemTitle
wrappingWidth: bind rectangleRef.width-10
textOrigin: TextOrigin.TOP
font: Font
{
name: "Arial"
size: 16
}
}
Panel
{
content: hyperlinkRef = Hyperlink
{
layoutX: bind (rectangleRef.layoutBounds.width-
hyperlinkRef.layoutBounds.width)/2-
hyperlinkRef.layoutBounds.minX
layoutY: bind (rectangleRef.layoutBounds.height-
hyperlinkRef.layoutBounds.height-10)-
hyperlinkRef.layoutBounds.minY
text: bind if (itemLink == null) then ""
else ">>> Check it out! <<<"
focusTraversable: false
action: function (): Void
{
AppletStageExtension.showDocument (itemLink,
"_blank")
}
}
}
]
}
}
}
FeedItemPanel subclasses javafx.scene.CustomNode, provides itemTitle and
itemLink variables for storing the current feed item's title and link, and provides a feedImageURL
variable for storing the URL of the newsfeed's logo image.
The feedImageURL variable is associated with a replace trigger that instantiates
javafx.scene.image.Image for the default logo, or for the newsfeed's logo whenever this variable is modified.
The Image reference is stored in imageRef.
Initially, I attached the trigger's code with a bind to the javafx.scene.image.ImageView instance's
image variable. However, I discovered that the newly-created Image's reference wasn't always
assigned to imageRef. Go figure!
CustomNode's create() function returns a Group instance that specifies this
component's user interface via instances of the following four classes:
-
javafx.scene.shape.Rectangleis used to present a background rectangle with a golden border and rounded corners. -
ImageViewis used to present the newsfeed logo. I found that some logos can get very large, and overflow theFeedItemPanel's display area. Rather than resort to explicit clipping, I take advantage offitWidthandfitHeightvariables to constrain the size of the displayed image, andpreserveRatioto ensure that the image looks good. -
javafx.scene.text.Textis used to present the current item's title. I originally intended to have theFeedItemPanelcomponent also present exception messages, and to center these messages within the panel. This is the reason for theif-elseexpression to whichText'slayoutYvariable is bound. -
javafx.scene.control.Hyperlinkis used to present generic link text and associate the current item's link with this text. I assignfalsetoHyperlink'sfocusTraversablevariable to prevent the hyperlink from receiving focus -- when this happens, the focus rectangle disappears, which can be disconcerting to the user.
The AppletStageExtension.showDocument (itemLink, "_blank") method call within the function that's assigned to
Hyperlink's action attribute ensures that the Web page at the associated itemLink
value is presented in a _blank browser window.
Finally, you might be wondering why I wrap the javafx.scene.control.Hyperlink instance within a
javafx.scene.layout.Panel instance. The brief answer is that I want FeedItemPanel to display
>>> Check it out! <<<, which won't happen without Panel.
The "When Resizables Are Not Managed by Containers" section of Amy Fowler's JavaFX1.2:
Layout blog post explains why Panel is required. Because Amy does an excellent job of explaining JavaFX, I
recommend that you read that section to discover the answer.
Testing FeedRead
FeedRead can be created to run as an application or an applet. However, you'll probably want to run it as an applet so that you can click on an item's link and view the item in its own Web page window. Just make sure to sign the applet's JAR file before deploying FeedRead.
I tested the FeedRead applet with JavaFX 1.2 and Mozilla Firefox 3.5.5 on a Windows XP SP3 platform. During my tests, I encountered a few oddities, which I discuss in this section. Most of these oddities probably result from JavaFX runtime bugs.
The first oddity involves the textbox not always receiving focus when FeedRead runs as an applet. You need to reload the applet or switch from the browser window to another window and then back to the browser window, to observe a focused textbox.
This oddity has been documented in the JIRA issue tracker for JavaFX as issue number JFXC-3431, "Signed javafx-applet does not get focus before html page area is clicked."
The next oddity deals with a button occasionally looking disabled when it's actually enabled. This sometimes happens with the Go button, but it can also happen with a navigation button, as revealed in Figure 2.

Figure 2. The < button appears to be disabled when it's actually enabled.
The third oddity deals with my not providing code to unescape an Atom feeditem's title -- notice the escaped
in Figure 3's title text. I leave it as an exercise for you to provide code that unescapes an Atom
feed's title.

Figure 3. Notice the escaped in the title text; also, the Go button appears to be disabled when it's actually enabled.
The previous oddities aren't as severe as specifying a URL such as http://www.x.com and selecting
Go, which results in Go remaining disabled. You must reload the
applet because onDone()/onException() is never invoked to re-enable this button.
One oddity that bugs me is receiving an empty string from getMessage() (from within onException()).
Because it's disconcerting to see an error dialog without any text, modify FeedRead to test for this possibility, and present
a generic error message if "" is returned.
Entering nothing into the textbox and selecting Go causes RssTask or
AtomTask to throw IllegalArgumentException, preventing Go from being
re-enabled. It's too bad that onException() isn't invoked.
Finally, pasting HTML into the textbox and clicking Go can cause the textbox to lockup. You'll
probably notice thrown IllegalArgumentExceptions with the message TextHitInfo is out of range. It
would be nice to disable copy-and-paste.
Conclusion
Now that you've explored FeedRead, you might want to extend this example. If you need some ideas, check out the RSS newsreaders shown in Rakesh Menon's RSS Viewer - JavaFX to JavaScript Communication sample article and the RSS Reader in JavaFX YouTube video.
Resources
- Sample code for this article
- Amy Fowler's JavaFX1.2: Layout blog post
- Atom specification
- Learn about JavaFX's APIs for Reading RSS and Atom Newsfeeds
- JavaFX 1.2.1 API Documentation
- JavaFX issues tracker at JIRA
- JavaFX's official Website
- JFXC-3431: Signed javafx-applet does not get focus before html page area is clicked
- Mark Macumber's JavaFX and RSS
- Rakesh Menon's RSS Viewer - JavaFX to JavaScript Communication sample article
- RSS specification
- Wikipedia's Atom (standard) entry
- Wikipedia's RSS entry
- Youtube's RSS Reader in JavaFX video
- Login or register to post comments
- Printer-friendly version
- 5781 reads



