Spring MVC RSS ATOM Content Negotiation Example
In this tutorial we show a Spring MVC RSS ATOM Content Negotiation example. Using Content Negotiation we can serve different versions of a document (or resource) at the same URI. Depending on the HTTP Accept header, a path file extension in the URL, a specific request parameter we return a view accordingly or if none of the previous are presented, we fall back on a default view.
Rss and Atom are used to create web feeds. These feeds are used to publish frequently updated information, typically found on blogs, news sites etc. These feeds contain information about the article like: title, link, description, content, etc.. Later on we create an RSS View, an Atom view and a JSP view, depending on the previous parameters we serve the correct view.
Maven Dependencies
We use Apache Maven to manage our project. Add the following dependencies and maven will automatically resolve them for us. Make sure the com.rometools:rome
dependency resides on the classpath. We are using this library to create the Rss and Atom views.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.memorynotfound.spring.mvc</groupId>
<artifactId>rss-view-resolver</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>SPRING-MVC - ${project.artifactId}</name>
<url>https://memorynotfound.com</url>
<packaging>war</packaging>
<properties>
<encoding>UTF-8</encoding>
<spring.version>4.2.6.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring libraries -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- rss and atom -->
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.6.0</version>
</dependency>
<!-- servlet api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Spring MVC RSS ATOM Content Negotiation – Configuration
When we extend from the WebMvcConfigurerAdapter
class, we can override the configureContentNegotiation(...)
method. Using this method we can configure the content negotiation using the ContentNegotiationConfigurer
. We can modify the default content type that’ll be served when none of the previous parameters are passed in the request. Here we can also give a priority to the different strategies.
Next, we register the different view which we’ll create later in this tutorial.
package com.memorynotfound.config;
import com.memorynotfound.view.AtomView;
import com.memorynotfound.view.RssView;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.*;
@EnableWebMvc
@Configuration
@ComponentScan("com.memorynotfound")
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.defaultContentType(MediaType.TEXT_HTML)
.parameterName("type")
.favorParameter(true)
.ignoreUnknownPathExtensions(false)
.ignoreAcceptHeader(false)
.useJaf(true);
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/views/", ".jsp");
registry.enableContentNegotiation(
new RssView(),
new AtomView());
}
}
This is the equivalent Spring XML Configuration.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven/>
<context:component-scan base-package="com.memorynotfound" />
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="defaultContentType" value="TEXT_HTML"/>
<property name="parameterName" value="type"/>
<property name="favorParameter" value="true"/>
<property name="ignoreUnknownPathExtensions" value="false"/>
<property name="ignoreAcceptHeader" value="false"/>
<property name="useJaf" value="true"/>
</bean>
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="com.memorynotfound.view.RssView"/>
<bean class="com.memorynotfound.view.AtomView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp prefix="/WEB-INF/views/" suffix=".jsp"/>
</mvc:view-resolvers>
</beans>
The DispatcherServlet
needs to be registered. This servlet will acts like a front-controller, dispatching the requests to the correct controller methods annotated with the @RequestMapping
. We configure the DispatcherServlet
programatically by extending from the AbstractAnnotationConfigDispatcherServletInitializer
and pass the location of the spring configuration file.
package com.memorynotfound.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class ServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
}
RSS Document
To demonstrate a simple Rss or Atom feed, we created a simple Document
POJO with some basic meta-data. Typically this would be an article, blog post or other information that can be shared.
package com.memorynotfound.model;
import java.util.Date;
public class Document {
private String feedId;
private String title;
private String description;
private Date pubDate;
private String link;
public Document(String feedId, String title, String description, Date pubDate, String link) {
this.feedId = feedId;
this.title = title;
this.description = description;
this.pubDate = pubDate;
this.link = link;
}
public String getFeedId() {
return feedId;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public Date getPubDate() {
return pubDate;
}
public String getLink() {
return link;
}
}
RSS View
Create an Rss View by extending from the AbstractRssView
and override the buildFeedMetadata
and buildFeedItems
methods. These methods populate the rss feed.
package com.memorynotfound.view;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.memorynotfound.model.Document;
import org.springframework.web.servlet.view.feed.AbstractRssFeedView;
import com.rometools.rome.feed.rss.Channel;
import com.rometools.rome.feed.rss.Content;
import com.rometools.rome.feed.rss.Item;
public class RssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel channel,
HttpServletRequest request) {
channel.setTitle("Spring MVC RSS ATOM Content Negotiation Example");
channel.setLink("https://memorynotfound.com/spring-mvc-rss-atom-content-negotiation/");
channel.setDescription("RSS ATOM View Resolvers");
channel.setLastBuildDate(new Date());
channel.setTtl(1800);
}
@Override
@SuppressWarnings("unchecked")
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
List<Item> items = new ArrayList<Item>();
Object ob = model.get("documents");
if (ob instanceof List){
List<Document> documents = (List<Document>)ob;
for (Document document : documents){
// content item
Content content = new Content();
content.setValue(document.getDescription());
// item
Item item = new Item();
item.setTitle(document.getTitle());
item.setContent(content);
item.setPubDate(document.getPubDate());
item.setLink(document.getLink());
items.add(item);
}
}
return items;
}
}
ATOM View
Create an Atom view by extending from the AbstractAtomFeedView
and override the buildFeedMetadata
and buildFeedEntries
methods. These methods populate the atom feed.
package com.memorynotfound.view;
import com.memorynotfound.model.Document;
import com.rometools.rome.feed.atom.Content;
import com.rometools.rome.feed.atom.Entry;
import com.rometools.rome.feed.atom.Feed;
import com.rometools.rome.feed.atom.Link;
import com.rometools.rome.feed.synd.SyndPerson;
import com.rometools.rome.feed.synd.SyndPersonImpl;
import org.springframework.web.servlet.view.feed.AbstractAtomFeedView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
public class AtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model, Feed feed,
HttpServletRequest request) {
// content
Content content = new Content();
content.setValue("ATOM View");
feed.setSubtitle(content);
feed.setTitle("Spring MVC RSS ATOM Content Negotiation Example");
// links
Link link = new Link();
link.setHref("https://memorynotfound.com/feed");
link.setRel("self");
link.setTitle("ATOM feed URL");
feed.setAlternateLinks(Arrays.asList(link));
// meta-data
feed.setUpdated(new Date());
}
@Override
@SuppressWarnings("unchecked")
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
List<Entry> entries = new ArrayList<Entry>();
Object ob = model.get("documents");
if (ob instanceof List) {
List<Document> documents = (List<Document>) ob;
for (Document document : documents) {
// entry
Entry entry = new Entry();
entry.setId(document.getFeedId());
entry.setPublished(document.getPubDate());
entry.setTitle(document.getTitle());
// content
Content content = new Content();
content.setValue(document.getDescription());
entry.setSummary(content);
// links
Link link = new Link();
link.setHref(document.getLink());
entry.setAlternateLinks(Arrays.asList(link));
// authors
SyndPerson author = new SyndPersonImpl();
author.setName("Memorynotfound");
author.setEmail("[email protected]");
entry.setAuthors(Arrays.asList(author));
entries.add(entry);
}
}
return entries;
}
}
Controller Endpoint
The DocumentController
initializes a list of documents and passes the documents to the Model
. Note that no code is added here to server different views, this is all handled by the content negotiation mechanism of Spring MVC which we configured earlier.
package com.memorynotfound.controller;
import com.memorynotfound.model.Document;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Controller
@RequestMapping("/documents")
public class DocumentController {
List<Document> documents = Arrays.asList(
new Document("1", "title1", "description1", new Date(), "https://memorynotfound.com"),
new Document("2", "title2", "description2", new Date(), "https://memorynotfound.com")
);
@RequestMapping(method = RequestMethod.GET)
public String getDocuments(Model model) {
model.addAttribute("documents", documents);
return "index";
}
}
Content Negotiation Demo
Next, we demonstrate how we can use the content negotiation we configured earlier. Like we mentioned in the introduction, we can negotiate content in different ways. Using either the HTTP Accept header, a path file extension in the URL, a specific request parameter or the default.
HTTP Accept Header
We request an Rss feed by passing the application/rss+xml – which is the Mime type of Rss – to the HTTP Accept Header.
curl -H "Accept: application/rss+xml" http://localhost:8081/spring-mvc-rss-atom/documents
The previous curl command gives us the following response.
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>Spring MVC RSS ATOM Content Negotiation Example</title>
<link>https://memorynotfound.com/spring-mvc-rss-atom-content-negotiation/</link>
<description>RSS ATOM View Resolvers</description>
<lastBuildDate>Thu, 19 May 2016 08:16:05 GMT</lastBuildDate>
<ttl>1800</ttl>
<item>
<title>title1</title>
<link>https://memorynotfound.com</link>
<content:encoded>description1</content:encoded>
<pubDate>Thu, 19 May 2016 08:15:22 GMT</pubDate>
</item>
<item>
<title>title2</title>
<link>https://memorynotfound.com</link>
<content:encoded>description2</content:encoded>
<pubDate>Thu, 19 May 2016 08:15:22 GMT</pubDate>
</item>
</channel>
</rss>
We request an Atom feed by passing the application/atom+xml – which is the Mime type of Atom – to the HTTP Accept Header.
curl -H "Accept: application/atom+xml" http://localhost:8081/spring-mvc-rss-atom/documents
The previous curl command gives us the following response.
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Spring MVC RSS ATOM Content Negotiation Example</title>
<link rel="self" href="https://memorynotfound.com/feed" title="ATOM feed URL" />
<subtitle>ATOM View</subtitle>
<updated>2016-05-19T08:15:28Z</updated>
<entry>
<title>title1</title>
<link rel="alternate" href="https://memorynotfound.com" />
<author>
<name>Memorynotfound</name>
<email>[email protected]</email>
</author>
<id>1</id>
<published>2016-05-19T08:15:22Z</published>
<summary>description1</summary>
</entry>
<entry>
<title>title2</title>
<link rel="alternate" href="https://memorynotfound.com" />
<author>
<name>Memorynotfound</name>
<email>[email protected]</email>
</author>
<id>2</id>
<published>2016-05-19T08:15:22Z</published>
<summary>description2</summary>
</entry>
</feed>
Default
URL: http://localhost:8081/spring-mvc-rss-atom/documents
File extension in URL
URL: http://localhost:8081/spring-mvc-rss-atom/documents.atom
Request Parameter
URL: http://localhost:8081/spring-mvc-rss-atom/documents?type=rss
That’s very interesting. Would love to try it myself tho.