Friday, September 07, 2007

Creating RSS Reader Component Using SEAM and ROME

I've been working lately on an enterprise application which will be used for developing and maintaining an organization's website. The application is being written using JSF, JBoss SEAM 1.2.1 GA, Facelets, ROME, Ajax4Jsf, and deploying to Glassfish V1.

There was a requirement to have an RSS reader as well as the ability to create RSS feeds. I found that the Rome Project had all of the tools which I was going to require to make this function. It is a great RSS library, and if you code in Java and use RSS then I definitely recommend it.

I've created an RSS reader which uses an Oracle database back end to store a table of RSS feeds to parse. There is also a table of pages in the database, and each RSS feed record is tied to one or more of the pages. The RSS web page component can go on any page...as long as the page is registered in the database. An administrator then has the ability to add RSS feeds to be read on that page. The component uses AJAX to display a news title for 10 seconds, and then read the database and parse the next RSS entry in the feed. Once the component has read all of the entries for a particular feed, it looks for more feeds registered to that page. If it finds more then it will parse all entries within that feed and so on. In the end, the RSS reader is a component which simply displays news from one or more RSS feed sources using AJAX.

The Database

create table rss_feed as(
FEED_ID NUMBER,
CONTENT_PAGE_ID NUMBER,
FEED_URL VARCHAR2(1000),
FEED_NAME VARCHAR2(500));

alter table rss_feed
add constraint rss_feed_pk
primary key(feed_id);

create sequence rssapp_rss_feed_seq
start with 1
increment by 1;

(This assumes that you have a table which stores each page name along with a unique page id)


The Code

Since we are using EJB3 technology with the SEAM framework, we will require an Entity class for persistence. We also need an EJB session bean which will contain all of the RSS logic, along with a local interface.

Entity Class - Straight forward. I use Netbeans to create the initial class from the database, and then I add the SEAM annotations.

/*
* RssAppRssFeed.java
*/

package org.jj.entity;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;

@Entity
@Scope(ScopeType.PAGE)
@Name("RssAppRssFeed")
@Table(name = "RSSAPP_RSS_FEED")
@NamedQueries( {
@NamedQuery(name = "RssAppRssFeed.findByFeedId", query = "SELECT f FROM RssAppRssFeed f WHERE f.feedId = :feedId"),
@NamedQuery(name = "RssAppRssFeed.findByContentPageId", query = "SELECT f FROM RssAppRssFeed f WHERE f.contentPageId = :contentPageId"),
@NamedQuery(name = "RssAppRssFeed.findByFeedUrl", query = "SELECT f FROM RssAppRssFeed f WHERE f.feedUrl = :feedUrl"),
@NamedQuery(name = "RssAppRssFeed.findByFeedName", query = "SELECT f FROM RssAppRssFeed f WHERE f.feedName = :feedName")
})
public class RssAppRssFeed implements Serializable {

@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE,
generator="RssApp_rss_feed_seq_generator")
@SequenceGenerator(name="RssApp_rss_feed_seq_generator",sequenceName="RssApp_rss_feed_seq", allocationSize=1)
@Column(name = "FEED_ID", nullable = false)
private BigDecimal feedId;

@Column(name = "CONTENT_PAGE_ID")
private BigInteger contentPageId;

@Column(name = "FEED_URL")
private String feedUrl;

@Column(name = "FEED_NAME")
private String feedName;

/** Creates a new instance of RssAppRssFeed */
public RssAppRssFeed() {
}

/**
* Creates a new instance of RssAppRssFeed with the specified values.
* @param feedId the feedId of the RssAppRssFeed
*/
public RssAppRssFeed(BigDecimal feedId) {
this.feedId = feedId;
}

/**
* Gets the feedId of this RssAppRssFeed.
* @return the feedId
*/
public BigDecimal getFeedId() {
return this.feedId;
}

/**
* Sets the feedId of this RssAppRssFeed to the specified value.
* @param feedId the new feedId
*/
public void setFeedId(BigDecimal feedId) {
this.feedId = feedId;
}

/**
* Gets the contentPageId of this RssAppRssFeed.
* @return the contentPageId
*/
public BigInteger getContentPageId() {
return this.contentPageId;
}

/**
* Sets the contentPageId of this RssAppRssFeed to the specified value.
* @param contentPageId the new contentPageId
*/
public void setContentPageId(BigInteger contentPageId) {
this.contentPageId = contentPageId;
}

/**
* Gets the feedUrl of this RssAppRssFeed.
* @return the feedUrl
*/
public String getFeedUrl() {
return this.feedUrl;
}

/**
* Sets the feedUrl of this RssAppRssFeed to the specified value.
* @param feedUrl the new feedUrl
*/
public void setFeedUrl(String feedUrl) {
this.feedUrl = feedUrl;
}

/**
* Gets the feedName of this RssAppRssFeed.
* @return the feedName
*/
public String getFeedName() {
return this.feedName;
}

/**
* Sets the feedName of this RssAppRssFeed to the specified value.
* @param feedName the new feedName
*/
public void setFeedName(String feedName) {
this.feedName = feedName;
}

/**
* Returns a hash code value for the object. This implementation computes
* a hash code value based on the id fields in this object.
* @return a hash code value for this object.
*/
@Override
public int hashCode() {
int hash = 0;
hash += (this.feedId != null ? this.feedId.hashCode() : 0);
return hash;
}

/**
* Determines whether another object is equal to this RssAppRssFeed. The result is
* true if and only if the argument is not null and is a RssAppRssFeed object that
* has the same id field values as this object.
* @param object the reference object with which to compare
* @return true if this object is the same as the argument;
* false otherwise.
*/
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof RssAppRssFeed)) {
return false;
}
RssAppRssFeed other = (RssAppRssFeed)object;
if (this.feedId != other.feedId && (this.feedId == null || !this.feedId.equals(other.feedId))) return false;
return true;
}

/**
* Returns a string representation of the object. This implementation constructs
* that representation based on the id fields.
* @return a string representation of the object.
*/
@Override
public String toString() {
return "org.jj.entity.RssAppRssFeed[feedId=" + feedId + "]";
}

}

EJB Session Bean - Once again, I use Netbeans to generate the initial session bean from the entity class and I modify for SEAM. I then added all of the essential methods and logic for the RSS in working with ROME. You will notice that I did not use SEAM's @In @Out annotations on some of the variables...that is because I had issues when doing so. The code that I post here does work as expected. You will also see some doubleSubmitIndex variables...this is due to an issue I've seen with Ajax4Jsf. When I use the a4j:poll, the page seems to invoke the server method twice. It causes the RSS reader to skip entries within the feed. Therefore, I coded a work around which is kind of messy, but it works.

/*
* FeedReader.java
*/

package org.jj.beans;


import com.sun.net.ssl.SSLContext;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import java.net.URL;
import java.io.InputStreamReader;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.jj.entity.RssAppContentPage;
import org.jj.entity.RssAppRssFeed;
import org.jj.interfaces.FeedLocal;
import org.jj.utility.RssAppCommonTasks;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.datamodel.DataModel;

/**
*
*/
@Stateful
@Scope(ScopeType.SESSION)
@Name("rssReader")
@Interceptors({org.jboss.seam.ejb.SeamInterceptor.class})
public class FeedReader implements FeedLocal {

@PersistenceContext(unitName="rssApp-ejbPU-FESSPROD")
private EntityManager em;

@DataModel
public List
feedList=null;

private List
rssFeedList = null;
private String pageName = null;

@Out(required=false)
private Feed feedBean;
private String feedText = null;
private Date feedDate = null;
private String feedTitle = null;
private int feedCounter;
private int feedEntryCounter;
private int feedIndex;
private int feedEntryIndex;
private int doubleSubmitIndex;
private int doubleSubmitFeedIndex;

private Query qry;

public void readFeed() {
pageName = RssAppCommonTasks.obtainPageName();
if (!pageName.equals("underMaintenance")){
qry = em.createQuery("select object(p) from RssAppContentPage as p " +
"where p.contentPage = :pageName")
.setParameter("pageName", pageName);
RssAppContentPage page = (RssAppContentPage) qry.getSingleResult();
qry = em.createQuery("select object(f) from RssAppRssFeed as f " +
"where f.contentPageId = :pageId")
.setParameter("pageId", page.getContentPageId());
rssFeedList = qry.getResultList();

// Parse rss feeds for page, if any
if (rssFeedList.size() > 0){
for(int index = 0; index <= rssFeedList.size()-1; index++){ boolean ok = false; int entryNumber = 0; int entryIndex = 0; RssAppRssFeed rssFeed = new RssAppRssFeed(); rssFeed = rssFeedList.get(index); System.out.println("BEGINNING RSS READ....."); if (rssFeed.getFeedUrl() != null) { try { feedList = null; URL feedUrl = new URL(rssFeed.getFeedUrl()); SyndFeedInput input = new SyndFeedInput(); SyndFeed feed = input.build(new XmlReader(feedUrl)); entryNumber = feed.getEntries().size(); initializeList(); for (entryIndex = 0; entryIndex <= entryNumber; entryIndex++){ System.out.println("Reading..."); feedBean = new Feed(); feedBean.setAuthor(feed.getAuthor()); feedBean.setDescription(feed.getDescription()); feedBean.setEntry(((SyndEntryImpl)feed.getEntries().get(entryIndex)).getTitle()); feedBean.setEntryLink(((SyndEntryImpl)feed.getEntries().get(entryIndex)).getLink()); feedBean.setEntryUpdatedDate(((SyndEntryImpl)feed.getEntries().get(entryIndex)).getPublishedDate()); feedBean.setTitle(feed.getTitle()); feedBean.setUri(feed.getUri()); feedList.add(feedBean); } ok = true; } catch (Exception ex) { ex.printStackTrace(); System.out.println("ERROR: "+ex.getMessage()); } } if (!ok) { System.out.println(); System.out.println("FeedReader reads and prints any RSS/Atom feed type."); System.out.println("The first parameter must be the URL of the feed to read."); System.out.println(); } } } } } /** * This method is invoked via ajax from the front end page in order to read * a new feed into the display. The ajax front end causes a double submit * to this method, so we need to adjust counters such that they will only update * on every other submit. We do this by using the doubleSubmitIndex numbers. */ public void updateFeed(){ if (doubleSubmitIndex > 1){
doubleSubmitIndex = 0;
}
if (doubleSubmitFeedIndex > 1){
doubleSubmitFeedIndex = 0;
}
pageName = RssAppCommonTasks.obtainPageName();

if (!pageName.equals("underMaintenance")){
qry = em.createQuery("select object(p) from RssAppContentPage as p " +
"where p.contentPage = :pageName")
.setParameter("pageName", pageName);

RssAppContentPage page = (RssAppContentPage) qry.getSingleResult();

qry = em.createQuery("select object(f) from RssAppRssFeed as f " +
"where f.contentPageId = :pageId")
.setParameter("pageId", page.getContentPageId());
rssFeedList = qry.getResultList();

/*
* Obtain a list of rss feeds for a particular web page. If feeds
* exist for that page, then enter the feed parse logic.
*/
if (rssFeedList.size() > 0){
feedCounter = rssFeedList.size();
if(this.getFeedIndex() > feedCounter - 1){
this.setFeedIndex(0);
}

boolean ok = false;

int entryIndex = 0;

/*
* Obtain all feed entries for this particular feed index
*/
RssAppRssFeed rssFeed = new RssAppRssFeed();
RssFeed = rssFeedList.get(getFeedIndex());

System.out.println("Feed Index: " + getFeedIndex() + " Entry Index: " + getFeedEntryIndex());

if (rssFeed.getFeedUrl() != null) {
try {
feedList = null;
URL feedUrl = new URL(rssFeed.getFeedUrl());
String feedEntryDateText = null;
String feedEntryText = null;
String feedEntryUrl = null;
SyndFeedInput input = new SyndFeedInput();
SyndFeed feed = input.build(new XmlReader(feedUrl));
/*
* Obtain count of all feed entries
*/
feedEntryCounter = feed.getEntries().size();
initializeList();

if(getFeedEntryIndex() > 0){
if (getFeedEntryIndex() > feedEntryCounter - 1){
setFeedEntryIndex(0);
}
}
System.out.println("Reading...");
feedBean = new Feed();
feedBean.setAuthor(feed.getAuthor());
feedBean.setDescription(feed.getDescription());
feedBean.setEntry(((SyndEntryImpl)feed.getEntries().get(getFeedEntryIndex())).getTitle());
feedBean.setEntryLink(((SyndEntryImpl)feed.getEntries().get(getFeedEntryIndex())).getLink());
feedBean.setEntryUpdatedDate(((SyndEntryImpl)feed.getEntries().get(getFeedEntryIndex())).getPublishedDate());
feedBean.setTitle(feed.getTitle());
setFeedTitle(feed.getTitle());
feedBean.setUri(feed.getUri());
feedList.add(feedBean);
this.feedDate = feedBean.getEntryUpdatedDate();
feedText = "" +
feedBean.getEntry() + "
";


ok = true;

if(doubleSubmitIndex > 0) {
this.feedEntryIndex++;
doubleSubmitIndex++;

// If all entries within current feedIndex have been read, move on
// to the next feed and start with entry zero.
if (feedEntryIndex > feedEntryCounter - 1){
this.feedIndex++;
this.feedEntryIndex = 0;
}
} else {
doubleSubmitIndex++;
}

} catch (Exception ex) {
ex.printStackTrace();
System.out.println("ERROR: "+ex.getMessage());
}
}

if (!ok) {
System.out.println();
System.out.println("FeedReader reads and prints any RSS/Atom feed type.");
System.out.println("The first parameter must be the URL of the feed to read.");
System.out.println();
}

}
}
}



private void initializeList(){
if (feedList == null){
feedList = new ArrayList();
}
}

public String getFeedText() {
return feedText;
}

public void setFeedText(String feedText) {
this.feedText = feedText;
}

public String getFeedTitle() {
return feedTitle;
}

public void setFeedTitle(String feedTitle) {
this.feedTitle = feedTitle;
}

public int getFeedIndex() {
return feedIndex;
}

public void setFeedIndex(int feedIndex) {
this.feedIndex = feedIndex;
}

public int getFeedEntryIndex() {
return feedEntryIndex;
}

public void setFeedEntryIndex(int feedEntryIndex) {
this.feedEntryIndex = feedEntryIndex;
}

public Date getFeedDate() {
return feedDate;
}

public void setFeedDate(Date feedDate) {
this.feedDate = feedDate;
}

@Remove @Destroy
public void destroy(){}


}


Local Interface - Nothing exciting in the local interface

/**
* FeedLocal.java
*/
package org.jj.interfaces;

import java.util.Date;

public interface FeedLocal {
void readFeed();

java.lang.String getFeedText();

void setFeedText(String feedText);

void updateFeed();

void destroy();

int getFeedIndex();

void setFeedIndex(int feedIndex);

Date getFeedDate();

void setFeedDate(Date feedDate);

java.lang.String getFeedTitle();

void setFeedTitle(String feedTitle);
}


Utility Class - Here is an excerpt from my utility class. This code obtains the current page name from the FacesContext.

public final class RssAppCommonTasks {

public static String obtainPageName(){

String currentPageName = null;
Query qry = null;
String tempPageName = FacesContext.getCurrentInstance().getViewRoot().getViewId();
if (tempPageName.lastIndexOf("/") > 0) {
tempPageName = tempPageName.substring(tempPageName.lastIndexOf("/") + 1).replace(".seam","").replace(".xhtml","");
currentPageName = tempPageName;
System.out.println("tempPageName: " + currentPageName);
} else {
System.out.println("view id: " + FacesContext.getCurrentInstance().getViewRoot().getViewId());
System.out.println("updated view id: " +
FacesContext.getCurrentInstance().getViewRoot().getViewId().replace("/","").replace(".seam","").replace(".xhtml",""));

currentPageName = FacesContext.getCurrentInstance().getViewRoot().getViewId().replace("/","").replace(".seam","").replace(".xhtml","");
}

return currentPageName;
}

***
}




pages.xml - Each page which contains the RSS component code will require an element within the pages.xml file to invoke server-side code.

<page id="/myPage.xhtml" action="#{rssReader.updateFeed}">

Component code in the XHTML page - I haven't yet created a separate component/tag for this code, but it should work by simply copying and pasting this code in each page which uses the reader. The back end code then determines which page is calling the code and parses the feeds in the database which are tied to that page.

<div id="autoreader">
<a4j:poll interval="10000"
reRender="autoReaderText, autoReaderDate, feedTitle"
action="#{rssReader.updateFeed}">
</a4j:poll>
<p align="center" class="sub_head_sub"><br />
<h:outputText id="feedTitle"
value="#{rssReader.feedTitle}"/>
</p>

<table id="feedList" name="feedList">
<td bgcolor="#CCFFCC" style="width: 100px">
<h:outputText id="autoReaderDate"
value="#{rssReader.feedDate}"/>
</td>
<td bgcolor="#CCFFCC" style="width: 400px">
<h:outputText id="autoReaderText"
escape="false"
value="#{rssReader.feedText}"/>
</td>
</table>
</div>

That is all there is to it. There is a bit of code to write, but the end result is very appealing. Once complete, one can add or remove feeds from the database and have them automatically appear on the desired pages.

2 comments:

  1. Can you post a zip of your netbeans project?

    ReplyDelete
  2. Unfortunately, I cannot. This RSS solution is only a fragment of an enterprise application which I have written. Parts of the application are proprietary and cannot be shared.

    If you have any questions, I'd be happy to help.

    ReplyDelete

Please leave a comment...