Nowadays, an almost standard feature of web applications is to provide RSS feeds, irrespective of whether it is for a blog or some location-based service. Most clients can handle RSS out of the box. The Play examples only carry an example with hand crafted RSS feeds around. This example shows how to use a library for automatic RSS feed generation by getting the newest 20 post entities and rendering it either as RSS, RSS 2.0 or Atom feed.
You can find the source code of this example in the chapter2/render-rss
directory.
As this recipe makes use of the ROME library to generate RSS feeds, you need to download ROME and its dependency JDOM first. You can use the Play dependency management feature again. Put this in your conf/dependencies.yml
:
require: - play - net.java.dev.rome -> rome 1.0.0
Now as usual a test comes first:
public classFeedTest extends FunctionalTest { @Test public void testThatRss10Works() throws Exception { Response response = GET("/feed/posts.rss"); assertIsOk(response); assertContentType("application/rss+xml", response); assertCharset("utf-8", response); SyndFeed feed = getFeed(response); assertEquals("rss_1.0", feed.getFeedType()); } @Test public void testThatRss20Works() throws Exception { Response response = GET("/feed/posts.rss2"); assertIsOk(response); assertContentType("application/rss+xml", response); assertCharset("utf-8", response); SyndFeed feed = getFeed(response); assertEquals("rss_2.0", feed.getFeedType()); } @Test public void testThatAtomWorks() throws Exception { Response response = GET("/feed/posts.atom"); assertIsOk(response); assertContentType("application/atom+xml", response); assertCharset("utf-8", response); SyndFeed feed = getFeed(response); assertEquals("atom_0.3", feed.getFeedType()); } private SyndFeedgetFeed(Response response) throws Exception { SyndFeedInput input = new SyndFeedInput(); InputSource s = new InputSource(IOUtils.toInputStream(getContent(response))); return input.build(s); } }
This test downloads three different kinds of feeds, rss1
, rss2
, and atom
feeds, and checks the feed type for each. Usually you should check the content as well, but as most of it is made up of random chars at startup, it is dismissed here.
The first definition is an entity resembling a post:
@Entity public class Post extends Model { public String author; public String title; public Date createdAt; public String content; public static List<Post>findLatest() { return Post.findLatest(20); } public static List<Post>findLatest(int limit) { return Post.find("order by createdAt DESC").fetch(limit); } }
A small job to create random posts on application startup, so that some RSS content can be rendered from application start:
@OnApplicationStart public class LoadDataJob extends Job { // Create random posts public void doJob() { for (int i = 0 ; i < 100 ; i++) { Post post = new Post(); post.author = "Alexander Reelsen"; post.title = RandomStringUtils.randomAlphabetic(RandomUtils.nextInt(50)); post.content = RandomStringUtils.randomAlphabetic(RandomUtils.nextInt(500)); post.createdAt = new Date(new Date().getTime() + RandomUtils.nextInt(Integer.MAX_VALUE)); post.save(); } } }
You should also add some metadata in the conf/application.conf
file:
rss.author=GuybrushThreepwood rss.title=My uber blog rss.description=A blog about very cool descriptions
The routes
file needs some controllers for rendering the feeds:
GET / Application.index GET /feed/posts.rss Application.renderRss GET /feed/posts.rss2 Application.renderRss2 GET /feed/posts.atom Application.renderAtom GET /post/{id} Application.showPost
The Application controller source code looks like this:
import static render.RssResult.*; public class Application extends Controller { public static void index() { List<Post> posts = Post.findLatest(100); render(posts); } public static void renderRss() { List<Post> posts = Post.findLatest(); renderFeedRss(posts); } public static void renderRss2() { List<Post> posts = Post.findLatest(); renderFeedRss2(posts); } public static void renderAtom() { List<Post> posts = Post.findLatest(); renderFeedAtom(posts); } public static void showPost(Long id) { List<Post> posts = Post.find("byId", id).fetch(); notFoundIfNull(posts); renderTemplate("Application/index.html", posts); } }
You should also adapt the app/views/Application/index.html
template to show posts and to put the feed URLs in the header to make sure a browser shows the RSS logo on page loading:
#{extends 'main.html' /} #{set title:'Home' /} #{set 'moreHeaders' } <link rel="alternate" type="application/rss+xml" title="RSS 1.0 Feed" href="@@{Application.renderRss2()}" /> <link rel="alternate" type="application/rss+xml" title="RSS 2.0 Feed" href="@@{Application.renderRss()}" /> <link rel="alternate" type="application/atom+xml" title="Atom Feed" href="@@{Application.renderAtom()}" /> #{/set} #{list posts, as:'post'} <div> <h1>#{a @Application.showPost(post.id)}${post.title}#{/a}</h1><br /> by ${post.author} at ${post.createdAt.format()} <div>${post.content.raw()}</div> </div> #{/list}
You also have to change the default app/views/main.html
template, from which all other templates inherit to include the moreHeaders
variable:
<html> <head> <title>#{get 'title' /}</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> #{get 'moreHeaders' /} <link rel="shortcut icon" type="image/png" href="@{'/public/images/favicon.png'}"> </head> <body> #{doLayout /} </body> </html>
The last part is the class implementing the different renderFeed
methods. This is again a Result
class:
public class RssResult extends Result { private List<Post> posts; private String format; public RssResult(String format, List<Post> posts) { this.posts = posts; this.format = format; } public static void renderFeedRss(List<Post> posts) { throw new RssResult("rss", posts); } public static void renderFeedRss2(List<Post> posts) { throw new RssResult("rss2", posts); } public static void renderFeedAtom(List<Post> posts) { throw new RssResult("atom", posts); } public void apply(Request request, Response response) { try { SyndFeed feed = new SyndFeedImpl(); feed.setAuthor(Play.configuration.getProperty("rss.author")); feed.setTitle(Play.configuration.getProperty("rss.title")); feed.setDescription(Play.configuration.getProperty("rss.description")); feed.setLink(getFeedLink()); List<SyndEntry> entries = new ArrayList<SyndEntry>(); for (Post post : posts) { String url = createUrl("Application.showPost", "id", post.id.toString()); SyndEntry entry = createEntry(post.title, url, post.content, post.createdAt); entries.add(entry); } feed.setEntries(entries); feed.setFeedType(getFeedType()); setContentType(response); SyndFeedOutput output = new SyndFeedOutput(); String rss = output.outputString(feed); response.out.write(rss.getBytes("utf-8")); } catch (Exception e) { throw new UnexpectedException(e); } } private SyndEntrycreateEntry (String title, String link, String description, Date createDate) { SyndEntry entry = new SyndEntryImpl(); entry.setTitle(title); entry.setLink(link); entry.setPublishedDate(createDate); SyndContententryDescription = new SyndContentImpl(); entryDescription.setType("text/html"); entryDescription.setValue(description); entry.setDescription(entryDescription); return entry; } private void setContentType(Response response) { ... } private String getFeedType() { ... } private String getFeedLink(){ ... } private String createUrl(String controller, String key, String value) { ... } }
This example is somewhat long at the end. The post entity is a standard model entity with a helper method to find the latest posts. The LoadDataJob
fills the in-memory database on startup with hundreds of random posts.
The conf/routes
file features showing an index page where all posts are shown, as well as showing a specific post and of course showing all three different types of feeds.
The controller makes use of the declared findLatest()
method in the post entity to get the most up-to-date entries. Furthermore the showPost()
method also utilizes the index.html
template so you do not need to create another template to view a single entry. All of the used renderFeed
methods are defined in the FeedResult
class.
The index.html
template file features all three feeds in the header of the template. If you take a look at app/views/main.html
, you might notice the inclusion of the moreHeaders
variable in the header. Using the @@ reference to a controller in the template creates absolute URLs, which can be utilized by any browser.
The FeedResult class begins with a constructor and the three static methods used in the controller, which render RSS, RSS 2.0, or Atom feeds appropriately.
The main work is done in the apply()
method of the class. A SyndFeed object is created and filled with meta information like blog name and author defined in the application.conf
file.
After that a loop through all posts generates entries, each including the author, content, title, and the generated URL. After setting the content type and the feed type—RSS or atom—the data is written to the response output stream.
The helper methods have been left out to save some lines inside this example. The setContentType()
method returns a specific content type, which is different for RSS and atom feeds. The getFeedType()
method returns "rss_2.0", "rss_1.0", or "atom_0.3" depending on the feed to be returned. The getFeedLink()
method returns an absolute URL for any of the three feed generating controller actions. The createUrl()
method is a small helper to create an absolute URL with a parameter which in this case is an ID. This is needed to create absolute URLs for each post referenced in the feed.
The example also uses ROME to extract the feed data again in the test, which is not something you should do to ensure the correct creation of your feed. Either use another library, or, if you are proficient in checking corner cases by hand, do it manually.
This is (as with most of the examples here) only the tip of the iceberg. Again, you could also create a template to achieve this, if you wanted to keep it simple. The official documentation lists some of the preparing steps to create your own templates ending with .rss
at http://www.playframework.org/documentation/1.2/routes#Customformats
This implementation is implementation specific. You could make it far more generic with the use of annotations at the Post entity:
@Entity public class Post extends Model { @FeedAuthor public String author; @FeedTitle public String title; @FeedDate public Date createdAt; @FeedContent public String content; }
Then you could change the render signature to the following:
public RssResult(String format, List<? extends Object> data) { this.data = data; this.format = format; } public static void renderFeedRss(List<? extends Object> data) { throw new RssResult("rss", data); }
By using the generics API you could check for the annotations defined in the Post entity and get the content of each field.
ROME comes with a bunch of additional modules. It is pretty easy to add GeoRSS information or MediaWiki-specific RSS. This makes it pretty simple to extend features of your feeds.
Especially RSS URLs are frequently under heavy loads – one misconfigured client type can lead to some sort of denial of service attack on your system. So again a good advice is to cache here. You could cache the SyndFeed object or the rss string created out of it. However, since play 1.1 you could also cache the complete controller output. See Basics of caching at the beginning of Chapter 4 for more information about that.
18.117.75.70