Skip to main content

Reading Newsfeeds in JavaFX with FeedRead

December 23, 2009

{cs.r.title}







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 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:

  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 instantiating RssTask.
  5. Invoke update() on the RssTask instance so that the onDone() 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.
  6. From within RssTask's onException() function, invoke the goAtom() function to
    instantiate AtomTask and invoke its update() function if onException()'s
    e argument's getMessage() 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. When RssTask's or AtomTask's onException() 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. 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/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 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.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 the
    FeedItemPanel's display area. Rather than resort to explicit clipping, I take advantage of fitWidth
    and fitHeight 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 the
    FeedItemPanel 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's focusTraversable 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 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 href="#resources">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 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


width="1" height="1" border="0" alt=" " />
Jeff Friesen is a freelance software developer and educator specializing in Java technology. Check out his site at javajeff.mb.ca.
Related Topics >> GUI   |   Programming   |   Web Applications   |   Web Services and XML   |   Featured Article   |