Home | Send Feedback

Produce RSS and Atom feeds with Spring framework

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

Creating an 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.

<dependency>
  <groupId>com.rometools</groupId>
  <artifactId>rome</artifactId>
  <version>1.12.2</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 an 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;
  }

FeedController.java

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;
  }

FeedController.java

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 static WireFeed createWireFeed(String feedType) {

    SyndFeed feed;
    if (feedType.equals("rss_2.0")) {
      feed = new CustomFeedEntry();
    }
    else {
      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/");

    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);

    Date publishDate = new Date();

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

    SyndEntry entry;
    if (feedType.equals("rss_2.0")) {
      entry = new CustomSyndEntry();
    }
    else {
      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();
  }

FeedController.java

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();
    }

FeedController.java

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

FeedController.java

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);

FeedController.java

When you validate the feeds again with the W3C Feed Validation Service, and you use the "Validate by Direct Input" method, you 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("======================================");
      }
    }

Parse.java

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