Micronaut RSS

Eases the creation of RSS feeds with Micronaut

Version: 4.3.0

1 Introduction

Micronaut RSS eases the creation of RSS feeds with Micronaut.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 What's New?

Micronaut RSS 2.0 uses Micronaut 2.0.

A new module, rss-core, contains core RSS related file. rss module depends on rss-core and HTTP related classes such as FeedController.

4 Breaking Changes

4.1 Breaking Changes in Micronaut RSS 3.0.0

  • Classes, constructors, etc that have been deprecated in previous versions of Micronaut RSS have been removed.

  • RssFeedProvider returns a Publisher

4.2 Breaking Changes in Micronaut RSS 2.0.0

:micronaut-itunespodcast depends only on micronaut-rss-core. You may want to use it in combination with micronaut-rss if you want to use the FeedController.

The artifacts' maven group has been changed to micronaut-rss. The next table shows the maven coordinates changes:

Old

New

io.micronaut.configuration:micronaut-rss

io.micronaut.rss:micronaut-rss

io.micronaut.configuration:micronaut-itunespodcast

io.micronaut.rss:micronaut-itunespodcast

io.micronaut.rss:rss-core

io.micronaut.rss:rss-core

5 RSS 2.0

implementation("io.micronaut.rss:micronaut-rss")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-rss</artifactId>
</dependency>

This modules exposes a controller FeedController which exposes feeds as described by RSS 2.0 Specification.

The following table displays several characteristics which can be configured:

🔗
Table 1. Configuration Properties for FeedControllerConfigurationProperties
Property Type Description

micronaut.rss.enabled

boolean

Whether FeedController should be enabled. Default value (true).

micronaut.rss.path

java.lang.String

FeedController requires two beans to be present RssFeedProvider and RssFeedRenderer. While DefaultRssFeedRenderer provides a default implementation for RssFeedRenderer, you will need to provide an implementation for RssFeedProvider.

For example, if you wanted to generate a feed as the one described in the RSS 2.0 specifiation’s sample you could create the next bean:

@Singleton
class MockRssFeedProvider implements RssFeedProvider {
    RssChannel rssChannel = RssChannel.builder("Liftoff News", "http://liftoff.msfc.nasa.gov/", "Liftoff to Space Exploration.")
            .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 4, 0, 0), ZoneId.of("GMT")))
            .lastBuildDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 9, 41, 1), ZoneId.of("GMT")))
            .docs("http://blogs.law.harvard.edu/tech/rss")
            .generator("Weblog Editor 2.0")
            .managingEditor("editor@example.com")
            .webMaster("webmaster@example.com")
            .item(RssItem.builder()
                .title("Star City")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp")
                .description("How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\"&gt;Star City&lt;/a&gt;.")
               .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 3, 9, 39, 21), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573")
                .build())
            .item(RssItem.builder()
                .description("Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 30, 11, 6, 42), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/30.html#item572")
                .build())
            .item(RssItem.builder()
                .title("The Engine That Does More")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp")
                .description("Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly.  The proposed VASIMR engine would do that.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 27, 8, 37, 32), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/27.html#item571")
            .build())
            .item(RssItem.builder()
                .title("Astronauts' Dirty Laundry")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp")
                .description("Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them.  Instead, astronauts have other options.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 20, 8, 56, 02), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/20.html#item570")
                .build())
            .build();

    @Override
    @SingleResult
    public Publisher<RssChannel> fetch() {
        return Publishers.just(rssChannel);
    }

    @Override
    @SingleResult
    public Publisher<RssChannel> fetchById(Serializable id) {
        if(id != null && id.equals("1")) {
            return Publishers.just(rssChannel);
        }
        return Publishers.empty();
    }
}

6 Itunes Podcast RSS Feed

implementation("io.micronaut.rss:micronaut-itunespodcast")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-itunespodcast</artifactId>
</dependency>

An Itunes podcast feed, is a valid RSS 2.0 feed with additional tags.

For example, the following bean will generate the Itunes Podcast RSS Feed’s sample

@Singleton
public class DefaultRssFeedProvider implements RssFeedProvider {

    @Override
    @SingleResult
    public Publisher<RssChannel> fetch() {
        return Publishers.just(ItunesPodcast.builder()
                .title("Hiking Treks")
                .link("https://www.apple.com/itunes/podcasts/")
                .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
                .copyright("&#8471; &amp; &#xA9; 2017 John Appleseed")
                .description("Love to get outdoors and discover nature&apos;s treasures? Hiking Treks is the show for you. We review hikes and excursions, review outdoor gear and interview a variety of naturalists and adventurers. Look for new episodes each week.")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .author("The Sunset Explorers")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .type(ItunesPodcastType.SERIAL)
                .owner(ItunesPodcastOwner.builder()
                        .name("Sunset Explorers")
                        .email("mountainscape@icloud.com")
                        .build())
                .image(RssChannelImage
                        .builder("Hiking Treks", "http://podcasts.apple.com/resources/example/hiking_treks/images/cover_art.jpg", "https://www.apple.com/itunes/podcasts/")
                        .build())
                .summary("Love to get outdoors and discover nature&apos;s treasures? Hiking Treks is the show for you. We review hikes and excursions, review outdoor gear and interview a variety of naturalists and adventurers. Look for new episodes each week.")
                .category(Arrays.asList(ItunesPodcastCategory.SPORTS_AND_RECREATION_OUTDOOR.getCategories()))
                .explicit(false)
                .item(ItunesPodcastEpisode.builder("Hiking Treks Trailer")
                        .episodeType(ItunesPodcastEpisodeType.TRAILER)
                        .author("The Sunset Adventurers")
                        .subtitle("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .summary("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .description("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .contentEncoded("<![CDATA[The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>]]>")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 4, 12, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("17:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Mt. Hood, Oregon")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(2)
                        .title("S02 EP04 Mt. Hood, Oregon")
                        .author("The Sunset Explorers")
                        .subtitle("Tips for trekking around the tallest mountain in Oregon")
                        .summary("Tips for trekking around the tallest mountain in Oregon")
                        .description("Tips for trekking around the tallest mountain in Oregon")
                        .contentEncoded("Tips for trekking around the tallest mountain in Oregon")
                        .enclosure(RssItemEnclosure.builder()
                                .length(8727310)
                                .type("audio/x-m4a")
                                .url("http://example.com/podcasts/everything/mthood.m4a")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170606.m4a")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 6, 06, 12, 0, 0), ZoneId.of("GMT")))
                        .duration("17:04")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Bouldering Around Boulder")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(2)
                        .title("S02 EP03 Bouldering Around Boulder")
                        .author("The Sunset Adventurers")
                        .subtitle("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .summary("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .description("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("video/mp4")
                                .url("http://example.com/podcasts/boulder.mp4")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170530.mp4")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 30, 13, 0, 0), ZoneId.of("GMT")))
                        .duration("06:27")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Caribou Mountain, Maine")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(2)
                        .title("S02 EP02 Caribou Mountain, Maine")
                        .author("The Sunset Adventurers")
                        .subtitle("Put your fitness to the test with this invigorating hill climb.")
                        .summary("Put your fitness to the test with this invigorating hill climb.")
                        .description("Put your fitness to the test with this invigorating hill climb.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("audio/x-m4v")
                                .url("http://example.com/podcasts/everything/caribou.m4v")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170523.m4v")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 23, 02, 0, 0), ZoneId.of("GMT")))
                        .duration("04:34")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Stawamus Chief")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(2)
                        .title("S02 EP01 Stawamus Chief")
                        .author("The Sunset Adventurers")
                        .subtitle("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .summary("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .description("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .contentEncoded("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170516.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 16, 02, 0, 0), ZoneId.of("GMT")))
                        .duration("13:24")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Kuliouou Ridge Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(1)
                        .title("S01 EP04 Kuliouou Ridge Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .summary("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .description("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .contentEncoded("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160509.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 10, 01, 15, 0), ZoneId.of("GMT")))
                        .duration("15:29")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Blood Mountain Loop")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(1)
                        .title("S01 EP03 Blood Mountain Loop")
                        .author("The Sunset Adventurers")
                        .subtitle("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .summary("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .description("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .contentEncoded("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160502.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 3, 01, 15, 0), ZoneId.of("GMT")))
                        .duration("24:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Garden of the Gods Wilderness")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(1)
                        .title("S01 EP02 Garden of the Gods Wilderness")
                        .author("The Sunset Adventurers")
                        .subtitle("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .summary("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .description("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .contentEncoded("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160425.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 26, 01, 15, 0), ZoneId.of("GMT")))
                        .duration("13:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Upper Priest Lake Trail to Continental Creek Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(1)
                        .title("S01 EP01 Upper Priest Lake Trail to Continental Creek Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .summary("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .description("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .contentEncoded("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418a.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 19, 01, 15, 0), ZoneId.of("GMT")))
                        .duration("23:59")
                        .explicit(false)
                        .contentEncoded("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>")
                        .build()).build());
    }

    @Override
    @SingleResult
    public Publisher<RssChannel> fetchById(Serializable id) {
        return Publishers.empty();
    }
}

7 JSON Feeds

JSON Feed is a format similar to RSS and Atom but in JSON.

The following dependency contains the core classes to build a JSON Feed.

implementation("io.micronaut.rss:micronaut-jsonfeed-core")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-jsonfeed-core</artifactId>
</dependency>

To get such JSON:

{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "My Example Feed",
    "home_page_url": "https://example.org/",
    "feed_url": "https://example.org/feed.json",
    "items": [
        {
            "id": "2",
            "content_text": "This is a second item.",
            "url": "https://example.org/second-item"
        },
        {
            "id": "1",
            "content_html": "<p>Hello, world!</p>",
            "url": "https://example.org/initial-post"
       }
    ]
}        

You can write:

JsonFeed feed = JsonFeed.builder()
        .version("https://jsonfeed.org/version/1.1")
        .title("My Example Feed")
        .homePageUrl("https://example.org/")
        .feedUrl("https://example.org/feed.json")
        .item(JsonFeedItem.builder()
                .id("2")
                .contentText("This is a second item.")
                .url("https://example.org/second-item")
                .build())
        .item(JsonFeedItem.builder()
                .id("1")
                .contentHtml("<p>Hello, world!</p>")
                .url("https://example.org/initial-post")
                .build())
        .build()

7.1 JSON Feeds Endpoint

The following dependency contains functionality to expose an endpoint to return a JSON Feed:

implementation("io.micronaut.rss:micronaut-jsonfeed")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-jsonfeed</artifactId>
</dependency>

Your JSON Feed will be of content type application/json+feed.

You have to register an additional type for Micronaut’s json codec:

micronaut.codec.json.additional-types[0]=application/json+feed

Then, if you provide a bean of type JsonFeedProvider such as:

@Singleton
public class ExampleJsonFeedProvider implements JsonFeedProvider {

    @NonNull
    @SingleResult
    @Override
    public Publisher<JsonFeed> feed(@Nullable Integer maxNumberOfItems, @Nullable Integer pageNumber) {
        return Flux.create( emitter -> {
            emitter.next(JsonFeed.builder()
                    .version("https://jsonfeed.org/version/1.1")
                    .title("My Example Feed")
                    .homePageUrl("https://example.org/")
                    .feedUrl("https://example.org/feed.json")
                    .item(JsonFeedItem.builder()
                            .id("2")
                            .contentText("This is a second item.")
                            .url("https://example.org/second-item")
                            .build())
                    .item(JsonFeedItem.builder()
                            .id("1")
                            .contentHtml("<p>Hello, world!</p>")
                            .url("https://example.org/initial-post")
                            .build())
                    .build());
            emitter.complete();
        }, FluxSink.OverflowStrategy.ERROR);
    }
}
If the creation of the JSON feed is a blocking I/O operation, offload that tasks to a separate thread pool that does not block the Event loop.

A GET request to /feeds/json returns a 200 OK response with HTTP Header with name Content-Type with value application/json+feed and a JSON Payload in the body such as:

{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "My Example Feed",
    "home_page_url": "https://example.org/",
    "feed_url": "https://example.org/feed.json",
    "items": [
        {
            "id": "2",
            "content_text": "This is a second item.",
            "url": "https://example.org/second-item"
        },
        {
            "id": "1",
            "content_html": "<p>Hello, world!</p>",
            "url": "https://example.org/initial-post"
       }
    ]
}        

There are additional configuration options for the JSON Feed Controller:

🔗
Table 1. Configuration Properties for JsonFeedControllerConfigurationProperties
Property Type Description

jsonfeed.root-path

java.lang.String

Configures JsonFeedController rootpath. Default value "/feeds"

jsonfeed.path

java.lang.String

Configures JsonFeedController path. Default value "/json"

jsonfeed.enabled

boolean

Whether JsonFeedController should be enabled. Default value (true).

You can implement pagination by supplying maxNumberOfItems and pageNumber which will be passed to your implementation of JsonFeedProvider.

8 Repository

You can find the source code of this project in this repository: