Salesforce Apex RSS Reader for Visualforce

  • submit to reddit

Salesforce RSS Reader for Visualforce

Salesforce RSS Reader for Visualforce

You may have, at one time or another, had the need or notion to embed an RSS feed into a Visualforce page.  If you are anything like me, you immediately began searching for “salesforce rss reader” or “visualforce rss reader” or “salesforce apex rss reader” or “salesforce rss parser” without much luck, which is precisely why I created this post.

So what is RSS and what is the use case for including it inside of a Visualforce pages? If you’ve ever used a news reader application, it was powered by the RSS feeds of various websites or services. RSS, aka Really Simple Syndication, uses a standard XML format (RSS 2.0 spec will be used in this example) to provide access to data from applications such as blogs, stock quotes, weather websites, news headlines, and many more.  Being able to bring data from RSS feeds into your Visualforce pages gives you the ability to aggregate information from multiple sources, and because those resources use the same standard, you don’t have to develop independent processes for each resource. My goal, is to provide you with a utility that will make this integration even easier.

A few things to note: You are confined by the call out limits of Salesforce (as of 03/27/2012 it is a max of 10 web service calls per transaction, essentially a max of 10 RSS feeds per page).  Second, you need to manually configure the “Remote Site Settings” of your org to work with each feed resource.

Before we started, I would just like to point out a few handy services that make their content available through RSS feeds:

News feeds

Stock Quotes (most will allow you to get news feeds for specific stock symbols)

Weather

Other

Now, onto the fun part.

Salesforce.com Apex RSS Parser Utility

The purpose of this Apex utility class is to allow you to parse the XML data of an RSS feed using its own standards and store that data into a wrapper class that you can then use to display the data however you wish (e.g. in a Visualforce page). This class is built so that it accepts a single parameter, the RSS feed endpoint, below is the code including a test unit.

public class RSS {
	
	public class channel {
		public String title {get;set;}
		public String link {get;set;}
		public String description {get;set;}
		public String author {get;set;}
		public String category {get;set;}
		public String copyright {get;set;}
		public String docs {get;set;}
		public RSS.image image {get;set;}
		public list<RSS.item> items {get;set;}
		public channel() {
			items = new list<RSS.item>();
		}
	}
	
	public class image {
		public String url {get;set;}
		public String title {get;set;}
		public String link {get;set;}
	}
	
	public class item {
		public String title {get;set;}
		public String guid {get;set;}
		public String link {get;set;}
		public String description {get;set;}
		public String pubDate {get;set;}
		public String source {get;set;}
		public Date getPublishedDate() {
			Date result = (pubDate != null) ? Date.valueOf(pubDate.replace('T', ' ').replace('Z','')) : null;
			return result;
		}
		public DateTime getPublishedDateTime() {
			DateTime result = (pubDate != null) ? DateTime.valueOf(pubDate.replace('T', ' ').replace('Z','')) : null;
			return result;
		}
	}
	
	public static RSS.channel getRSSData(string feedURL) {
		
		HttpRequest req = new HttpRequest();
		req.setEndpoint(feedURL);
        req.setMethod('GET');
        
        Dom.Document doc = new Dom.Document();
		Http h = new Http();
		
		if (!Test.isRunningTest()){ 
			HttpResponse res = h.send(req);
            doc = res.getBodyDocument();
		} else {
			String xmlString = '<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:os="http://a9.com/-/spec/opensearch/1.1/"><channel><title>salesforce.com - Bing News</title><link>http://www.bing.com/news</link><description>Search Results for salesforce.com at Bing.com</description><category>News</category><os:totalResults>3370</os:totalResults><os:startIndex>0</os:startIndex><os:itemsPerPage>10</os:itemsPerPage><os:Query role="request" searchTerms="salesforce.com" /><copyright>These XML results may not be used, reproduced or transmitted in any manner or for any purpose other than rendering Bing results within an RSS aggregator for your personal, non-commercial use. Any other use requires written permission from Microsoft Corporation. By using these results in any manner whatsoever, you agree to be bound by the foregoing restrictions.</copyright><image><url>http://www.bing.com/s/a/rsslogo.gif</url><title>Bing</title><link>http://www.bing.com/news</link></image><docs>http://www.rssboard.org/rss-specification</docs><item><title>Salesforce.com Makes Friends With CIOs - Information Week</title><guid>http://informationweek.com/news/cloud-computing/software/232602782</guid><link>http://informationweek.com/news/cloud-computing/software/232602782</link><description>Parade of CIOs at CloudForce shows how social networking inroads are making Salesforce.com a larger part of the IT infrastructure. Salesforce.com isn&apos;t just for sales forces anymore. Its Chatter app has opened a social networking avenue into the enterprise ...</description><pubDate>2012-03-19T15:21:47Z</pubDate><source>Information Week</source></item></channel></rss>';
			doc.load(xmlString);
		}
		
		Dom.XMLNode rss = doc.getRootElement();
		//first child element of rss feed is always channel
		Dom.XMLNode channel = rss.getChildElements()[0];
		
		RSS.channel result = new RSS.channel();
		
		list<RSS.item> rssItems = new list<RSS.item>();
		
		//for each node inside channel
		for(Dom.XMLNode elements : channel.getChildElements()) {
			if('title' == elements.getName()) {
				result.title = elements.getText();
			}
			if('link' == elements.getName()) {
                result.link = elements.getText();
            }
            if('description' == elements.getName()) {
                result.description = elements.getText();
            }
            if('category' == elements.getName()) {
                result.category = elements.getText();
            }
            if('copyright' == elements.getName()) {
                result.copyright = elements.getText();
            }
            if('docs' == elements.getName()) {
                result.docs = elements.getText();
            }
            if('image' == elements.getName()) {
                RSS.image img = new RSS.image();
                //for each node inside image
                for(Dom.XMLNode xmlImage : elements.getChildElements()) {
                	if('url' == xmlImage.getName()) {
                		img.url = xmlImage.getText();
                	}
                	if('title' == xmlImage.getName()) {
                        img.title = xmlImage.getText();
                    }
                    if('link' == xmlImage.getName()) {
                        img.link = xmlImage.getText();
                    }
                }
                result.image = img;
            }
            
            if('item' == elements.getName()) {
            	RSS.item rssItem = new RSS.item();
            	//for each node inside item
            	for(Dom.XMLNode xmlItem : elements.getChildElements()) {
            		if('title' == xmlItem.getName()) {
            			rssItem.title = xmlItem.getText();
        			}
        			if('guid' == xmlItem.getName()) {
                        rssItem.guid = xmlItem.getText();
                    }
                    if('link' == xmlItem.getName()) {
                        rssItem.link = xmlItem.getText();
                    }
                    if('description' == xmlItem.getName()) {
                        rssItem.description = xmlItem.getText();
                    }
                    if('pubDate' == xmlItem.getName()) {
                        rssItem.pubDate = xmlItem.getText();
                    }
                    if('source' == xmlItem.getName()) {
                        rssItem.source = xmlItem.getText();
                    }
            	}
            	//for each item, add to rssItem list
            	rssItems.add(rssItem);
            }
            
		}
		//finish RSS.channel object by adding the list of all rss items
		result.items = rssItems;
		
		return result;
		
	}
	
	static testMethod void RSSTest() {
        RSS.channel chan = RSS.getRSSData('test');
        Date pDate = chan.items[0].getPublishedDate();
        DateTime pDateTime = chan.items[0].getPublishedDateTime();
    }

}

Visualforce RSS Reader

Now let’s say that you want to use the parser utility to display data in a Visualforce page. The usage is very simple, as you will see below. We will begin by building a controller for the page.


public class RSSNewsReader {
	
	public String rssQuery {get;set;}
	private String rssURL {get;set;}
	
	public RSSNewsReader() {
		
		rssURL = 'http://api.bing.com/rss.aspx?Source=News&Market=en-US&Version=2.0&Query=';
		rssQuery = 'salesforce.com'; //default on load
		
	}
	
	public RSS.channel getRSSFeed() {
		return RSS.getRSSData(rssURL + EncodingUtil.urlEncode(rssQuery,'UTF-8'));
	}
	
	static testMethod void RSSNewsReaderTest() {
        RSSNewsReader con = new RSSNewsReader();
        RSS.channel rssFeed = con.getRSSFeed();
    }

}

In the above example, I’m using the Bing news feed url, which accepts a query parameter. This allows me to accept user input in order to dynamically modify the feed end point and get the new data from the getRSSFeed() method. Below is the Visualforce page that goes with this controller.


<apex:page controller="RSSNewsReader" sidebar="false" showHeader="false" cache="false">
<style>
.ajax-loader {
    background: url({!$Resource.ajax_loader});
    display: inline-block;
    width: 16px;
    height: 11px;
}
</style>
<apex:image value="{!$Resource.ajax_loader}" style="visibility: hidden;" />

<apex:pageBlock id="rssBlock" tabStyle="Lead">
    
    <apex:form style="padding-bottom: 10px;">
	    <span>Bing News Query (Enter a Topic): </span>
	    <apex:inputText value="{!rssQuery}" />
	    <apex:commandButton value="Search Bing News" reRender="rssBlock" status="searchStatus" />
	    <apex:actionStatus id="searchStatus" startStyleClass="ajax-loader" />
    </apex:form>
    
    <apex:pageBlockSection title="Channel" columns="2">
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="title" />
            <apex:outputText value="{!RSSFeed.title}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="link" />
            <apex:outputText value="{!RSSFeed.link}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="description" />
            <apex:outputText value="{!RSSFeed.description}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="category" />
            <apex:outputText value="{!RSSFeed.category}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="docs" />
            <apex:outputText value="{!RSSFeed.docs}" />
        </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
    
    <apex:pageBlockSection columns="1">
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="copyright" />
            <apex:outputText value="{!RSSFeed.copyright}" />
        </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
    
    <apex:pageBlockSection title="Image" columns="2">
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="title" />
            <apex:outputText value="{!RSSFeed.image.title}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="url" />
            <apex:outputText value="{!RSSFeed.image.url}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="link" />
            <apex:outputText value="{!RSSFeed.image.link}" />
        </apex:pageBlockSectionItem>
        <apex:pageBlockSectionItem >
            <apex:outputLabel value="url image" />
            <apex:image value="{!RSSFeed.image.url}" />
        </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
    
    <apex:pageBlockSection title="Items" columns="1">
        
        <apex:pageBlockTable value="{!RSSFeed.items}" var="i">
            <apex:column headerValue="title">
                <apex:outputLink value="{!i.link}" target="_blank">{!i.title}</apex:outputLink>
            </apex:column>
            <apex:column headerValue="description" value="{!i.description}"/>
            <apex:column headerValue="pubDate" style="width: 140px;">
                <apex:outputText value="{0,date,MM/dd/yy h:mm:ss a}" >
                    <apex:param value="{!i.PublishedDateTime}" />
                </apex:outputText>
            </apex:column>
            <apex:column headerValue="source" value="{!i.source}" style="width: 140px;" />
        </apex:pageBlockTable>
    
    </apex:pageBlockSection>
    
</apex:pageBlock>
    
</apex:page>

To see the news reader in action, visit the Salesforce RSS Reader for Visualforce demo page. As always, I highly encourage your feedback. Put this solution to the harshest of tests, break it, and we will work on fixing it together.


I have been working with the Salesforce.com platform since 2003 in both administrative and development roles. I also have experience in web design working with HTML, CSS, Javascript, and PHP. I hold certification for the Salesforce.com Developer program. I am also an employee of Model Metrics, a Salesforce.com company.

  • http://twitter.com/ROIFactory ROI Factory

    Visualforce is cut off on right side, see lines 17, 74 & 82. Can you repost or somehow provide the few missing characters? Thanks!

  • http://twitter.com/ROIFactory ROI Factory

    “title” is missing in VF line 44

  • Sudhir

    Anthony – Thank you so much for this. But when i try to use WSJ feed. I keep getting an error.

    Invalid date/time: ue, 28 May 2013 19:55:56 ED

    An unexpected error has occurred. Your development organization has been notified. I am not sure if i am missing something. I tried playing with function getPublishedDate. But i seem to be missing something. Can you please guide me

    Sudhir

  • Pankaj Sontakke

    Hi Anthony,

    I am trying to do the same but I am getting following error:

    I do get the HttpResponse but not getting the Body Document. When I do so, I get theerror.

    “: Failed to parse XML due to: attribute value must start with quotation or apostrophe not e (position: DOCDECL seen”

Read previous post:
Salesforce Roll-Up Summary Utility
Salesforce Roll-up Summary Utility for Lookup Fields with Filter

A while back I wrote a Salesforce roll-up summary trigger which mimics the declarative roll-up summary functionality provided by Salesforce.com.  I received several comments...

Close