DWR + RSS = Slick

Yesterday Alec and I decided to convert one of our Tapestry pages over to use AJAX. The page in question is an RSS Feed portlet. Essentially it displays a list of RSS feeds and for each feed the current list of articles available from that feed as hyperlinks – pretty standard RSS stuff. It was working fine as a standard Tapestry page – the Page class did the bulk of the work of downloading the RSS xml files from the various sites and parsing them. We used Rome to do this. The only problem was that it was quite slow. Because this page is rendered as part of a portal page it was affecting the rendering time of every request. By using AJAX we were able to render the bulk of the page in the foreground and pull in the RSS feeds in the background and dynamically render them as they came in.

Here’s what was involved:
We created a DWR service called RssFeedService which provides one method – getFeed(String url). This method returns a javabean of type: RssFeedBean.
Here’s the code:

package ca.intelliware.ajax;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;

public class RssFeedService {
	public RssFeedBean getFeed(String url) {
		try {
			if (url != null) {
				URL feedUrl = new URL(url);
				SyndFeedInput input = new SyndFeedInput();
				SyndFeed feed = input.build(new InputStreamReader(feedUrl.openStream()));
				return new RssFeedBean(feed);
			} else {
				return null;
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (FeedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;

	}

}

Next we registered our service with DWR by adding it to the dwr.xml file:

<!DOCTYPE dwr PUBLIC
  "-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
  "http://www.getahead.ltd.uk/dwr/dwr10.dtd">

<dwr>
  <allow>
    <create creator="spring" javascript="RssFeedService">
      <param name="beanName"
        value="com.rbcdexia_is.investorportal.ajax.RssFeedService"/>
    </create>
    <convert converter="bean"
      match="com.rbcdexia_is.investorportal.ajax.RssFeedBean"/>
  </allow>
</dwr>

The bean class knows how to render itself as HTML. This part I’m still not happy with. It knows too much about the structure of the page into which it’s being injected. (Still some refactoring to do there.) Anyway, here’s the bean code:

package ca.intelliware.ajax;

import java.util.Iterator;

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;

public class RssFeedBean {

	private final SyndFeed feed;

	public RssFeedBean(SyndFeed feed) {
		this.feed = feed;
	}

	public String getHtml() {
		StringBuffer sb = new StringBuffer();
		sb.append(createFeedLink());
		for (Iterator i = feed.getEntries().iterator(); i.hasNext();) {
			sb.append(createEntryLink((SyndEntry) i.next()));
		}
		return sb.toString();
	}

	private String createEntryLink(SyndEntry entry) {
		StringBuffer sb = new StringBuffer();
		sb.append("<tr><td colspan="2">");
		sb.append("<a href="").append(entry.getLink()).append("" target="_blank">").append(entry.getTitle()).append("</a>");
		sb.append("</td></tr>n"); return sb.toString(); } private String createFeedLink() { StringBuffer sb = new StringBuffer(); sb.append("<tr><th class="feed" colspan="2">");
		sb.append("<a href="").append(feed.getLink()).append("" target="_blank" title="").append(feed.getDescription()).append("">");
		sb.append("<img src="").append(feed.getImage().getUrl()).append(""/>");
		sb.append(feed.getTitle());
		sb.append("</a>");
		sb.append("</th></tr>n");
		return sb.toString();
	}

}

The interesting part was the javascript stuff we had to generate. This part is handled by our Tapestry page. Essentially we have a for loop in our page and for each RSS feed the following method is called:

public String getScript() {
		StringBuffer sb = new StringBuffer();
		List<RssFeed> rssFeeds = getFeeds();
		if (!rssFeeds.isEmpty()) {
			sb.append("<script type="text/javascript" language="JavaScript">n"); sb.append("  function loadFeeds() {n"); int index = 0; for (Iterator i = rssFeeds.iterator(); i.hasNext(); index++) { RssFeed feed = (RssFeed) i.next(); sb.append("    RssFeedService.getFeed(function (rssFeedBean) { DWRUtil.setValue("feeds_").append(index).append("", rssFeedBean.html) }, '").append(feed.getUrl()).append("');n"); } sb.append("  }n"); sb.append("</script>n");
		}
		return sb.toString();
	}

An example of the resulting HTML looks like this:

<script type="text/javascript" language="JavaScript">
  function loadFeeds() {
    RssFeedService.getFeed(function (rssFeedBean) { DWRUtil.setValue("feeds_0", rssFeedBean.html) }, 'http://rss.cbc.ca/worldnews.xml');
    RssFeedService.getFeed(function (rssFeedBean) { DWRUtil.setValue("feeds_1", rssFeedBean.html) }, 'http://www.theglobeandmail.com/generated/rss/BN/International.xml');
    RssFeedService.getFeed(function (rssFeedBean) { DWRUtil.setValue("feeds_2", rssFeedBean.html) }, 'http://slashdot.org/index.rss');
  }
</script>
<table>
<thead>
<tr>
<th width="100%">News Feeds</th>
<th style="text-align: right"><a href=
"/portal/AssignFeeds.html">Edit</a></th>
</tr>
</thead>
<tbody id="feeds_0"></tbody>
<tbody id="feeds_1"></tbody>
<tbody id="feeds_2"></tbody>
</table>

So for each feed two snippets are created

  1. A call to our RssFeedService for the specified url and
  2. An empty table body element with a unique id is added to the table.

The part that we struggled with was how to inject the results returned from our service call into the page. Notice the first parameter to the service call. It’s an anonymous function that appends to the specified table body the value returned by calling getHtml() on the rssFeedBean which was returned from our service call. This callback mechanism is ubiquitous in the DWR paradigm but it took us a while to understand it’s impact on implementation. The other thing that we didn’t realize was that the DWR service calls are asynchronous. We should have realized that but we kind of forgot. 🙂

There’s probably some room for refactoring here – DWR provides a function to add rows to a table but it seemed more complicated than necessary for our needs, however it would probably reduce the amount of HTML code that our bean would have to construct.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply