Creating an RSS or Atom feed using the Spring Framework is straightforward. 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 Spring automatically registers them when it discovers the library on the classpath. All we have to do is add ROME as a dependency to pom.xml.
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>2.1.0</version>
</dependency>
Next, we need to create a controller and a mapping for RSS and/or Atom. For this example, we create the following two mappings.
@RestController
public class FeedController {
@GetMapping(path = "/rss")
public Channel rss() {
...
}
@GetMapping(path = "/atom")
public Feed atom() {
...
}
}
Alternatively, instead of using @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 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 how to create both an Atom and an RSS feed. It uses the classes from the ROME library and returns a Feed and a Channel.
The HTTP message converters mentioned above convert these objects into XML and send the response back to the client.
The Feed class 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://blog.rasc.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://blog.rasc.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://blog.rasc.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;
}
The Channel class 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://blog.rasc.ch/");
channel.setUri("https://blog.rasc.ch/");
Date postDate = new Date();
channel.setPubDate(postDate);
Item item = new Item();
item.setAuthor("Ralph");
item.setLink("https://blog.rasc.ch/p/1");
item.setTitle("1");
item.setUri("https://blog.rasc.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;
}
Produce with SyndFeed and SyndEntry ¶
When an application needs to produce both feeds, you otherwise end up writing a lot of very similar code. A better solution is to use the higher-level SyndFeed and SyndEntry API from ROME. 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 ("rss_2.0".equals(feedType)) {
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://blog.rasc.ch/");
feed.setAuthor("Ralph");
feed.setUri("https://blog.rasc.ch/");
AtomNSModule atomNSModule = new AtomNSModuleImpl();
String link = "rss_2.0".equals(feedType) ? "/synd_rss" : "/synd_atom";
atomNSModule.setLink("https://blog.rasc.ch" + link);
feed.getModules().add(atomNSModule);
Date publishDate = new Date();
List<SyndEntry> entries = new ArrayList<>();
SyndEntry entry;
if ("rss_2.0".equals(feedType)) {
entry = new CustomSyndEntry();
}
else {
entry = new SyndEntryImpl();
}
entry.setTitle("1");
entry.setAuthor("Ralph");
entry.setLink("https://blog.rasc.ch/p/1");
entry.setUri("https://blog.rasc.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();
}
The feed.createWireFeed() method is the key here. It creates a Feed or a Channel instance based on feedType.
WireFeed is the superclass of both Channel and Feed.
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 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>
According to the validator, an RSS feed should contain only one of these.
The problem is that the setPublishedDate() method from the SyndFeedImpl and SyndEntryImpl classes creates both dates. That is okay for Atom, but not for RSS.
To solve that, we create two subclasses, one for SyndFeedImpl and one for SyndEntryImpl, and override the setPublishedDate methods.
You can find the two classes on GitHub: CustomFeedEntry, CustomSyndEntry
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 ("rss_2.0".equals(feedType)) {
feed = new CustomFeedEntry();
}
else {
feed = new SyndFeedImpl();
}
SyndEntry entry;
if ("rss_2.0".equals(feedType)) {
entry = new CustomSyndEntry();
}
else {
entry = new SyndEntryImpl();
}
To fix the Missing atom:link with rel="self" validation warning, a bit more code is needed. The validator expects a link tag in both feeds that points to the feed itself, and ROME does not support creating such a link out of the box.
The solution comprises three new classes (AtomNSModule, AtomNSModuleGenerator, AtomNSModuleImpl) and a properties file (rome.properties) that are added to the project, plus the following new lines in the createWireFeed method.
AtomNSModule atomNSModule = new AtomNSModuleImpl();
String link = "rss_2.0".equals(feedType) ? "/synd_rss" : "/synd_atom";
atomNSModule.setLink("https://blog.rasc.ch" + link);
feed.getModules().add(atomNSModule);
When you validate the feeds again with the W3C Feed Validation Service and 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 href in the XML.
Consume ¶
ROME can not only produce RSS and Atom feeds, it can also consume them. The following example reads an Atom feed from usgs.gov that contains a list of earthquakes that occurred during the last hour. The SyndFeedInput class automatically recognizes the feed format and creates a SyndFeed plus, for each entry, a SyndEntry instance.
This part of the post is still valid today. Feed generation is a niche requirement now, but Spring's feed converters and ROME are still available if you need classic RSS or Atom output.
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("======================================");
}
}
You can find all the code mentioned in this post on GitHub.