Reading Newsfeeds in JavaFX with FeedRead Blog


    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.

    FeedRead's colorful user interface lets you navigate through RSS and Atom newsfeeds.
    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 andfeedread.png). Check out Main.fx:

    /* * Main.fx */ package feedread; import java.lang.Exception; import; import; import; import; import; import; 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: } 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 ofFeedItem instances (identifying each feed item'stitle and link), an indexinto 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 theGo button. This function performs the following tasks:

    1. Remove the previous newsfeed from the model: delete items; index = 0;
    2. Disable the Go button so that it cannot be clicked: buttonGoRef.disable = true;
    3. Prevent the user from shifting focus to this button via the keyboard: buttonGoRef.focusTraversable = false;
    4. Assume that the newsfeed is in RSS format by instantiatingRssTask.
    5. Invoke update() on the RssTaskinstance so that the onDone() function is invoked (if the newsfeed is in RSS format) -- it's important to re-enable theGo button when the newsfeed's items have been successfully obtained.
    6. From within RssTask's onException()function, invoke the goAtom() function to instantiateAtomTask and invoke its update() function if onException()'s e argument'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 with HttpRequest to obtain the newsfeed XML, and working with PullParser to parse enough of the feed to determine if it's Atom or RSS. WhenRssTask's or AtomTask'sonException() function is invoked, it's important to alert the user to the problem. This task is accomplished by invoking javafx.stage.Alert's public static void inform(String title, String message) method to present a dialog displaying getMessage()'s value. TheGo button is then re-enabled.

    The model's items sequence is populated inRssTask's onItem() andAtomTask'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 inAtomTask's onFeed() function viafeedImageURL = f.icon.uri;.

    At this point, the application's stage is created and its scene is laid out. The scene is organized within ajavafx.scene.layout.Flow instance, whoselayoutX and layoutY variables are initialized such that the scene is centered on the stage.

    Perhaps you're wondering why I subtractflowRef.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, setlayoutX/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 currentlayoutBounds.minX/minY location 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.minY

    Or, in the case of object literals, don't forget thebind:

    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 ajavafx.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 viaitems [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 subclassesjavafx.scene.CustomNode, providesitemTitle and itemLink variables for storing the current feed item's title and link, and provides afeedImageURL variable for storing the URL of the newsfeed's logo image.

    The feedImageURL variable is associated with a replace trigger that instantiatesjavafx.scene.image.Image for the default logo, or for the newsfeed's logo whenever this variable is modified. TheImage reference is stored inimageRef.

    Initially, I attached the trigger's code with abind to the javafx.scene.image.ImageViewinstance'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.Rectangle is used to present a background rectangle with a golden border and rounded corners.
    • ImageView is 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 of fitWidth andfitHeight variables to constrain the size of the displayed image, and preserveRatio to ensure that the image looks good.
    • javafx.scene.text.Text is used to present the current item's title. I originally intended to have theFeedItemPanel component also present exception messages, and to center these messages within the panel. This is the reason for the if-else expression to which Text's layoutY variable is bound.
    • javafx.scene.control.Hyperlink is used to present generic link text and associate the current item's link with this text. I assign false to Hyperlink'sfocusTraversable variable 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 toHyperlink'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 thejavafx.scene.control.Hyperlink instance within ajavafx.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.

    The < button appears to be disabled when it's actually enabled.
    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 &#13;in Figure 3's title text. I leave it as an exercise for you to provide code that unescapes an Atom feed's title.

    Notice that the Go button appears to be disabled when it's actually enabled.
    Figure 3. Notice the escaped &#13; 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 and selecting Go, which results in Go remaining disabled. You must reload the applet becauseonDone()/onException() is never invoked to re-enable this button.

    One oddity that bugs me is receiving an empty string fromgetMessage() (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 selectingGo causes RssTask orAtomTask to throwIllegalArgumentException, preventingGo from being re-enabled. It's too bad thatonException() isn't invoked.

    Finally, pasting HTML into the textbox and clickingGo 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.


    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.