Produce RSS and Atom feeds with Springframework

Published: January 24, 2017  •  Updated: February 16, 2018  •  spring, java

Creating a RSS or Atom feed with the Spring framework is quite easy. With version 3.0.2, Spring introduced two http message converters (RssChannelHttpMessageConverter and AtomFeedHttpMessageConverter) that translate return values of controller methods into the corresponding XML feed format.
Both converters depend on the ROME library and the Spring Framework does automatically registers these two converters when it discovers the library on the classpath. All we have to do is adding the ROME library as a dependency to our pom.xml (since 4.1 Spring depends on the rometools variant of ROME).

<dependency>
  <groupId>com.rometools</groupId>
  <artifactId>rome</artifactId>
  <version>1.11.0</version>
</dependency>	

Next we need to create a Controller and a mapping for RSS and/or Atom. For this example I create the following two mappings.

@RestController
public class FeedController {
  @GetMapping(path = "/rss")
  public Channel rss() {
    ...
  }

  @GetMapping(path = "/atom")
  public Feed atom() {
    ...
  }
}

Instead with the @RestController you could annotate the class with @Controller and the method with @ResponseBody.

@Controller
public class FeedController {
  @ResponseBody
  @GetMapping(path = "/rss")
  public Channel rss() {
     ...
  }
}

Both approaches do work, they signal Spring to convert the return value with a http message converter before sending the response back to the client.


Produce

The following example shows you how to create an Atom and a RSS feed. They use the classes from the ROME library and return a Feed and a Channel. The above mentioned http message converters convert these objects into XML and send the response back to the client.

The Feed represents an Atom feed and manages a collection of zero, one or many Entry objects.

@GetMapping(path = "/atom")
public Feed atom() {
  Feed feed = new Feed();
  feed.setFeedType("atom_1.0");
  feed.setTitle("Ralph's Blog");
  feed.setId("https://golb.hplar.ch/");

  Content subtitle = new Content();
  subtitle.setType("text/plain");
  subtitle.setValue("Blog about this and that");
  feed.setSubtitle(subtitle);
		
  Date postDate = new Date();
  feed.setUpdated(postDate);
				
  Entry entry = new Entry();
		
  Link link = new Link();
  link.setHref("https://golb.hplar.ch/p/1");
  entry.setAlternateLinks(Collections.singletonList(link));
  SyndPerson author = new Person();
  author.setName("Ralph");
  entry.setAuthors(Collections.singletonList(author));
  entry.setCreated(postDate);
  entry.setPublished(postDate);
  entry.setUpdated(postDate);
  entry.setId("https://golb.hplar.ch/p/1");
  entry.setTitle("1");
		
  Category category = new Category();
  category.setTerm("tag1");
  entry.setCategories(Collections.singletonList(category));
		
  Content summary = new Content();
  summary.setType("text/plain");
  summary.setValue("a short description");
  entry.setSummary(summary);

  feed.setEntries(Collections.singletonList(entry));
  return feed;
}

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/FeedController.java#L65-L105

The Channel is the Feed equivalent for RSS feeds. It manages a collection of zero, one or many Item objects.

@GetMapping(path = "/rss")
public Channel rss() {
    Channel channel = new Channel();
    channel.setFeedType("rss_2.0");
    channel.setTitle("Ralph's Blog");
    channel.setDescription("Blog about this and that");
    channel.setLink("https://golb.hplar.ch/");
    channel.setUri("https://golb.hplar.ch/");
		
    Date postDate = new Date();
    channel.setPubDate(postDate);
				
    Item item = new Item();
    item.setAuthor("Ralph");
    item.setLink("https://golb.hplar.ch/p/1");
    item.setTitle("1");
    item.setUri("https://golb.hplar.ch/p/1");
		
    com.rometools.rome.feed.rss.Category category = new com.rometools.rome.feed.rss.Category();
    category.setValue("tag1");
    item.setCategories(Collections.singletonList(category));
		
    Description descr = new Description();
    descr.setValue("a short description");
    item.setDescription(descr);
    item.setPubDate(postDate);

    channel.setItems(Collections.singletonList(item));
    return channel;
  }

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/FeedController.java#L34-L63

Produce with SyndFeed and SyndEntry

When an application needs to produce both feeds (RSS and Atom) then you have to write a lot of similar code with the approach above. A better solution is to use the higher level api SyndFeed and SyndEntry from the ROME library. This allows an application to write the feed producer code only once and then convert the SyndFeed object into a Channel for RSS or a Feed for Atom.

@GetMapping(path = "/synd_rss")
public Channel s_rss() {
  return (Channel) createWireFeed("rss_2.0");
}

@GetMapping(path = "/synd_atom")
public Feed s_atom() {
  return (Feed) createWireFeed("atom_1.0");
}

private WireFeed createWireFeed(String feedType) {
  SyndFeed feed = new SyndFeedImpl();
  feed.setFeedType(feedType);

  feed.setTitle("Ralph's Blog");
  feed.setDescription("Blog about this and that");
  feed.setLink("https://golb.hplar.ch/");
  feed.setAuthor("Ralph");
  feed.setUri("https://golb.hplar.ch/");

  Date publishDate = new Date();

  List<SyndEntry> entries = new ArrayList<>();

  SyndEntry entry = new SyndEntryImpl();
  entry.setTitle("1");
  entry.setAuthor("Ralph");
  entry.setLink("https://golb.hplar.ch/p/1");
  entry.setUri("https://golb.hplar.ch/p/1");
  entry.setPublishedDate(publishDate);
  entry.setUpdatedDate(publishDate);

  List<SyndCategory> categories = new ArrayList<>();
  SyndCategory category = new SyndCategoryImpl();
  category.setName("tag1");
  categories.add(category);
  entry.setCategories(categories);

  SyndContent description = new SyndContentImpl();
  description.setType("text/plain");
  description.setValue("the summary");
  entry.setDescription(description);

  entries.add(entry);

  feed.setPublishedDate(publishDate);

  feed.setEntries(entries);
  return feed.createWireFeed();
}

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/FeedController.java#L107-L174

The method feed.createWireFeed() is the key here. It creates a Feed or a Channel instance based on the feedType. WireFeed is the superclass for both Channel and Feed classes.


Valid format

When we validate the two synd_* feeds with https://validator.w3.org/feed/ we see several problems.

For the RSS feed the validator prints out these warnings:

line 9, column 4: A channel should not include both pubDate and dc:date [help]
   <dc:date>2017-01-24T10:20:34Z</dc:date>
    ^
line 18, column 6: An item should not include both pubDate and dc:date [help]
   <dc:date>2017-01-24T10:20:34Z</dc:date>
    ^
line 20, column 2: Missing atom:link with rel="self" [help]
 </channel>

And the for the Atom feed the validator reports this problem

line 2, column 0: Missing atom:link with rel="self" [help]
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/eleme ...

First we tackle the date problem. The RSS feed contains these two date lines

<pubDate>Tue, 24 Jan 2017 10:24:04 GMT</pubDate>
<dc:date>2017-01-24T10:24:04Z</dc:date>

but according to the validator a RSS feed should only contain one of these.

The problem is that the setPublishedDate() method from the SyndFeedImpl and SyndEntryImpl class creates both dates, this is okay for Atom but not for RSS. To solve that we create two subclasses one for SyndFeedImpl, one for SyndEntryImpl and overwrite the setPublishedDate methods. You find the two clases on GitHub: CustomFeedEntry, CustomSyndEntry

And then in the createWireFeed method we need to change the code so that the application instantiates these two implementations for the RSS feed.

  SyndFeed feed;
  if (feedType.equals("rss_2.0")) {
    feed = new CustomFeedEntry();
  }
  else {
    feed = new SyndFeedImpl();
  }

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/FeedController.java#L119-L125

   SyndEntry entry;
   if (feedType.equals("rss_2.0")) {
      entry = new CustomSyndEntry();
   }
   else {
      entry = new SyndEntryImpl();
   }

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/FeedController.java#L143-L149

To fix the Missing atom:link with rel="self" validation warning a bit more code is needed. The problem is that the validator expects a link tag in both feeds that points to the feed itself and ROME does not have support out of the box for creating such a link.

I found the solution for the problem in this blog post: https://www.gridshore.nl/2010/02/16/creating-a-w3c-validated-rss-feed-using-rome-and-spring-3/
The solution demonstrated there only fixed the RSS feed but after a few minor tweaks it fixes the Atom feed too.

The solution comprises three new classes (AtomNSModule, AtomNSModuleGenerator, AtomNSModuleImpl) and a properties file (rome.properties) that are added to the project and the following new lines added to the createWireFeed method.

AtomNSModule atomNSModule = new AtomNSModuleImpl();
String link = feedType.equals("rss_2.0") ? "/synd_rss" : "/synd_atom";
atomNSModule.setLink("https://golb.hplar.ch" + link);
feed.getModules().add(atomNSModule);

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/FeedController.java#L134-L137

When you validate the feeds again with the W3C Feed Validation Service and you use the "Validate by Direct Input" method you will see a new error: "Self reference doesn't match document location". The validator only recognizes the self link correctly when you validate the feed with the "Validate by URI" method and the URL you enter matches the link href in the XML.


Consume

The ROME library cannot only produce RSS and Atom feeds, it is also capable of consuming feeds. The following example reads an Atom feed from usgs.gov that contains a list of earthquakes that occurred during the last hour. The class SyndFeedInput automatically recognizes the feed format and creates a SyndFeed and for each entry (earthquake) a SyndEntry instance.

String url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.atom";

try (XmlReader reader = new XmlReader(new URL(url))) {
  SyndFeed feed = new SyndFeedInput().build(reader);
  System.out.println(feed.getTitle());
  System.out.println("=======================");
  for (SyndEntry entry : feed.getEntries()) {
    System.out.println(entry);
    System.out.println("======================================");
  }
}

https://github.com/ralscha/blog/blob/master/rss/src/main/java/ch/rasc/rss/Parse.java

You find all the code mentioned in this post on GitHub.