Modules with ROME
Just as RSS is extensible through modules, ROME supports an extension mechanism called modules. ROME modules are used to represent any element outside of the base set of elements defined for the SyndFeed, SyndEntry, Feed, Channel, EnTRy, and Item objects. We actually already used one of the modules that are included with the ROME distribution: Dublin Core. Because RSS 1.0 does not support any sort of date element either within a channel or item, the RSS 1.0 feed produced by our example feed generator contained the Dublin Core date element to define the date for the feed:
<dc:date>2006-08-03T02:51:45Z</dc:date>
When our SyndFeed object was converted to an RSS 1.0 Channel object, the Dublin Core module was used to store this date value. And when WireFeedOutput was given this Channel object, it saw that the Dublin Core module was present and had the Dublin Core module add this additional element to the output XML document. In addition to the Dublin Core and RSS 1.0 Syndication modules included with the ROME distribution, there are many useful modules available listed on the ROME Wiki, located at http://wiki.java.net/bin/view/Javawsxml/Rome. Currently these include ROME modules support these RSS modules:
- RSS 1.0 Content
- iTunes
- Slashcode
- Google Base
- Creative Commons
- MediaRSS
- GeoRSS
- Apple iPhoto Photocast
- A9 Open Search
ROME modules are packaged in regular JAR files. By simply adding the JAR file to your classpath, the module discovery mechanism used by the feed parser will be able to find it. We'll see how this happens later in the section "Creating a ROME Module." When creating a feed or entry object, it's necessary to add an instance of a module object. A module can be applied to a feed-level object (SyndFeed, Feed, and Channel), an entry-level object (SyndEntry, Entry, and Item), or both. Each ROME module includes one or two module interfaces and implementations of those classes. The module interface extends com.sun.syndication.feed.module.Module and the implementation class extends com.sun.syndication.feed.module.ModuleImpl. Modules can also include auxiliary classes to be used with the module interface. As you can see in Figures 12-1, 12-2, and 12-3, each of the SyndFeed, SyndEntry, Feed, Channel, Entry and Item classes have three module-related methods:
getModules( )
-
Returns a List of module objects
setModules(List list)
-
Replaces the current list of module objects
getModule(String uri)
-
Returns the module object associated with a particular namespace URI
To demonstrate the use of a ROME module, we'll create a podcast for submission to the iTunes Music Store using the ROME iTunes module. The iTunes module is one of the more complex ROME modules, so even if you aren't interested in creating a podcast, you'll be able to apply the concepts in this example to your use of other modules.
Creating a Podcast RSS Feed
As I mentioned in the "Blogs and Podcasting" sidebar earlier in this chapter, a podcast feed is an RSS feed that contains references to a multimedia file. Podcasting began with the use of the enclosure RSS 2.0 element, although there is now an RSS 1.0 module to support enclosures in RSS 1.0 documents (for more details, see http://www.xs4all.nl/~foz/mod_enclosure.html) For the purposes of our example, we'll create a RSS 2.0 feed. The RSS 2.0 enclosure element has three required attributes:
url
-
The URL of the referenced media file
length
-
The length of the media file in bytes
type
-
The MIME type of the media file
ROME includes an Enclosure class in the RSS data model (seen in ) and a SyndEnclosure interface in the format-independent data model (seen in ). Both have getters and setters for properties representing these attributes. Creating a simple podcast is just a matter of creating an RSS 2.0 channel where the items have enclosure elements, as seen in Example 12-6.
Example Generating a simplepodcast feed
package javaxml3; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.Collections; import com.sun.syndication.feed.rss.Channel; import com.sun.syndication.feed.rss.Description; import com.sun.syndication.feed.rss.Enclosure; import com.sun.syndication.feed.rss.Item; import com.sun.syndication.io.FeedException; import com.sun.syndication.io.WireFeedOutput; public class PodcastExample throws Exception { public static void main(String[] args) { Channel channel = new Channel("rss_2.0"); channel.setTitle("Example Podcast Feed"); channel.setDescription("Example podcast description."); channel.setLink("http://www.example.org"); Item item = new Item( ); item.setTitle("My First Podcast"); Description description = new Description( ); description.setType("text"); description.setValue("My first attempt at a podcast."); item.setDescription(description); Enclosure enclosure = new Enclosure( ); enclosure.setUrl("http://www.example.org/podcast/ep1.mp3"); enclosure.setType("audio/mpeg"); enclosure.setLength(4182295); item.getEnclosures( ).add(enclosure); channel.getItems( ).add(item); WireFeedOutput outputter = new WireFeedOutput( ); outputter.output(channel, new OutputStreamWriter(System.out)); } } |
The output of this class is:
<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"> <channel> <title>Example Podcast Feed</title> <link>http://www.example.org</link> <description>Example podcast description.</description> <item> <title>My First Podcast</title> <description>My first attempt at a podcast.</description> <enclosure url="http://www.example.org/podcast/ep1.mp3" length="4182295" type="audio/mpeg" /> </item> </channel> </rss>
Loading this feed into a podcatcher will have the podcatcher download the MP3 file from http://www.example.org/podcast/ep1.mp3. You can post a link to this RSS feed on your web site and your visitors can subscribe to it in their podcatcher. However, to reach a wider audience, you should submit your podcast feed to one or more podcast directories. A podcast directory is a categorized listing of podcast feeds. Popular podcast directories include Podcast Alley (http://www.podcastalley.com/), Odeo.com, Yahoo! (http://podcasts.yahoo.com/), and the iTunes Music Store. A podcast directory will use the description and categorization information contained within your RSS feed, but some directories, most significantly the iTunes Music Store, have additional requirements to add your feed to the directory. In the case of iTunes, Apple has created an RSS module, described at http://www.apple.com/itunes/podcasts/techspecs.html, to support the needs of the iTunes directory. This module includes elements to be included inside both channel and item elements. You must include support for this module to have your feed listed in the iTunes directory. The elements defined by the iTunes module are contained in Table 12-3.
Table 12-3. Elements defined by the iTunes module
Element name | Channel | Item | Required |
---|---|---|---|
author | Y | Y | N |
block | Y | Y | N |
category | Y | N | Y |
image | Y | N | N |
duration | N | Y | N |
explicit | Y | Y | Y (for channel) |
keywords | Y | Y | N |
new-feed-url | Y | N | N |
owner | Y | N | N |
subtitle | Y | Y | N |
summary | Y | Y | N |
The namespace URI for these elements is http://www.itunes.com/dtds/podcast-1.0.dtd. A ROME module that supports these elements is available from the ROME Wiki. The module includes two different interfaces: FeedInformation and EntryInformation to represent the elements within the iTunes RSS module. Both interfaces extend the ITunes interface , which defines the elements included at both levels of the feed. These interfaces and their associated implementation classes are in the com.sun.syndication.feed.module.itunes package, diagrammed in .
ROME iTunes module

We can use this module to add the new elements to our sample podcast feed. To start, we create an instance of the FeedInformationImpl class and add it to the list of modules for our Channel object:
FeedInformation feedInfo = new FeedInformationImpl( ); channel.getModules( ).add(feedInfo);
Adding this to the code in Example 12-6 immediately changes the output:
<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"> <channel> <title>Example Podcast Feed</title> <link>http://www.example.org</link> <description>Example podcast description.</description> <itunes:owner> <itunes:email /> <itunes:name /> </itunes:owner> <itunes:explicit>no</itunes:explicit> <itunes:keywords /> <item> <title>My First Podcast</title> <description>My first attempt at a podcast.</description> <enclosure url="http://www.example.org/podcast/ep1.mp3" length="4182295" type="audio/mpeg" /> </item> </channel> </rss>
Similarly, if we add the EntryInformationImpl module class to our Item object's module list, the output of that item includes some new elements:
<item> <title>My First Podcast</title> <description>My first attempt at a podcast.</description> <enclosure url="http://www.example.org/podcast/ep1.mp3" length="4182295" type="audio/mpeg" /> <itunes:explicit>no</itunes:explicit> <itunes:keywords /> </item>
Then, using the getters and setters shown in , we can set the specific element values for our channel:
feedInfo.setKeywords(new String[] { "Example", "Java", "XML" }); feedInfo.setOwnerEmailAddress("podcasts@example.org"); feedInfo.setOwnerName("Podcast Owner");
And also to our item:
entryInfo.setKeywords(new String[] { "Education" }); entryInfo.setAuthor("learning@example.org");
But before we can submit our updated feed to iTunes, we must add category information.
iTunes categories
The iTunes podcast directory is organized into a two-level hierarchy of categories. In other words, a sports-related podcast could be categorized in the Professional subcategory of the Sports category. The RSS 2.0 category element is just a string and Apple could have encoded the categories in a single element such as:
<category>Sports\Professional</category>
Instead, it choses to use a category element within the iTunes namespace that could be nested so that a feed could be categorized like this:
<itunes:category > <itunes:category /> </itunes:category>
Alternatively, no subcategories are available for some top-level categories:
<itunes:category />
To represent this, the ROME iTunes module contains classes named Category and Subcategory in the com.sun.syndication.feed.module.itunes.types package. A Category can contain a Subcategory, and a FeedInformation object can contain multiple Category objects. Putting all of our modifications in place gives us:
public static void main(String[] args) throws Exception { Channel channel = new Channel("rss_2.0"); channel.setTitle("Example Podcast Feed"); channel.setDescription("Example podcast description."); channel.setLink("http://www.example.org"); FeedInformation feedInfo = new FeedInformationImpl( ); channel.getModules( ).add(feedInfo); feedInfo.setKeywords(new String[] { "Example", "Java", "XML" }); feedInfo.setOwnerEmailAddress("podcasts@example.org"); feedInfo.setOwnerName("Podcast Owner"); Category category = new Category("Technology"); category.setSubcategory(new Subcategory("Podcasting")); feedInfo.getCategories( ).add(category); feedInfo.getCategories( ).add(new Category("Comedy")); Item item = new Item( ); item.setTitle("My First Podcast"); Description description = new Description( ); description.setType("text"); description.setValue("My first attempt at a podcast."); item.setDescription(description); Enclosure enclosure = new Enclosure( ); enclosure.setUrl("http://www.example.org/podcast/ep1.mp3"); enclosure.setType("audio/mpeg"); enclosure.setLength(4182295); item.getEnclosures( ).add(enclosure); EntryInformation entryInfo = new EntryInformationImpl( ); entryInfo.setKeywords(new String[] { "Education" }); entryInfo.setAuthor("learning@example.org"); item.getModules( ).add(entryInfo); channel.getItems( ).add(item); WireFeedOutput outputter = new WireFeedOutput( ); outputter.output(channel, new OutputStreamWriter(System.out)); }
This updated class outputs:
<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"> <channel> <title>Example Podcast Feed</title> <link>http://www.example.org</link> <description>Example podcast description.</description> <itunes:owner> <itunes:email>podcasts@example.org</itunes:email> <itunes:name>Podcast Owner</itunes:name> </itunes:owner> <itunes:category > <itunes:category /> </itunes:category> <itunes:category /> <itunes:explicit>no</itunes:explicit> <itunes:keywords>Example, Java, XML</itunes:keywords> <item> <title>My First Podcast</title> <description>My first attempt at a podcast.</description> <enclosure url="http://www.example.org/podcast/ep1.mp3" length="4182295" type="audio/mpeg" /> <itunes:author>learning@example.org</itunes:author> <itunes:explicit>no</itunes:explicit> <itunes:keywords>Education</itunes:keywords> </item> </channel> </rss>
This is ready to be submitted to Apple, leading you to fame and fortune in the world of podcasting. You can also use the iTunes module to access the iTunes elements in a feed you've parsed with ROME. For example, to retrieve the feed owner's email address, you would determine if the iTunes module was included in the feed and, if it was, call the getOwnerEmailAddress( ) method of the FeedInformation object, as seen in Example 12-7.
Example Using the iTunes module with input
package javaxml3; import org.xml.sax.InputSource; import com.sun.syndication.feed.WireFeed; import com.sun.syndication.feed.module.itunes.FeedInformation; import com.sun.syndication.feed.module.itunes.ITunes; import com.sun.syndication.io.FeedException; import com.sun.syndication.io.WireFeedInput; public class OutputOwnerEmail { public static void main(String[] args) throws FeedException { if (args.length != 1) { System.err.println("Usage: java javaxml3.OutputOwnerEmail [url]"); return; } WireFeedInput input = new WireFeedInput( ); // first, parse the feed WireFeed feed = input.build(new InputSource(args[0])); // second, get the module for the iTunes URI FeedInformation feedInfo = (FeedInformation) feed.getModule(ITunes.URI); if (feedInfo != null) System.out.println(feedInfo.getOwnerEmailAddress( )); else System.out.println("No iTunes module available"); } } |
Creating a ROME Module
ROME makes it very simple to create your own module. As noted above, the core of a module is an interface that encapsulates the metadata contained within the module elements. This interface must extend com.sun.syndication.feed.module.Module. As we saw with the iTunes module, some RSS modules are complex enough to need a different interface for feed-level elements and another for entry-level elements. In addition to this interface and an implementation of it, a module needs to define a class to output the module elements and a class to parse the module elements. To enable this input and output of module elements, ROME defines two interfaces: com.sun.syndication.io.ModuleGenerator and com.sun.syndication.io.ModuleParser. The Module, ModuleGenerator, and ModuleParser interfaces are diagrammed in .
ROME module interfaces

Beyond simply containing these interfaces and classes, your module needs to be discoverable by ROME. When a WireFeedInput or WireFeedOutput object is created, the constructor looks for files named rome.properties in the classpath. These files use the standard properties file syntax. The keys are constructed like:
<feedtype>.<feed or item>.<ModuleGenerator or ModuleParser>.classes
The values for these properties are one or more fully qualified class name(s). If there is more than one class, the class names can be separated with commas or spaces. For example, the rome.properties file from the iTunes module includes these lines to register the class that outputs the iTunes module elements:
rss_2.0.feed.ModuleGenerator.classes=\ com.sun.syndication.feed.module.itunes.io.ITunesGenerator rss_2.0.item.ModuleGenerator.classes=\ com.sun.syndication.feed.module.itunes.io.ITunesGenerator
And these lines to register the classes that parse feeds:
rss_2.0.feed.ModuleParser.classes=\ com.sun.syndication.feed.module.itunes.io.ITunesParser \ com.sun.syndication.feed.module.itunes.io.ITunesParserOldNamespace rss_2.0.item.ModuleParser.classes=\ com.sun.syndication.feed.module.itunes.io.ITunesParser \ com.sun.syndication.feed.module.itunes.io.ITunesParserOldNamespace
Because instances of the parser and generator classes are created by the ROME framework, it is necessary that they both have zero-argument, public constructors.
|
Once this properties file is populated with the correct properties, ROME will able to discover the generator and parsers associated with your module. To demonstrate module development, let's create a simple ROME module to store the location of a feed entry.
The ICBM module
The ICBM RSS module, documented at http://postneo.com/icbm, includes elements to store the latitude and longitude of a feed entry. For example, a blog entry could look like:
<item> <!-- some item element --> <icbm:latitude>26.03226</icbm:latitude> <icbm:longtitude>-80.307344</icbm:longtitude> <!-- other item elements --> </item>
To create a ROME module to represent these elements, we first create a module interface. In addition to getters and setters for the two element values, we'll also include constants to store this module's namespace URI and preferred namespace prefix:
package javaxml3.icbm; import com.sun.syndication.feed.module.Module; public interface ICBMModule extends Module { public static final String PREFIX = "icbm"; public static final String URI = "http://postneo.com/icbm/"; public void setLatitude(float latitude); public void setLongitude(float longitude); public float getLatitude( ); public float getLongitude( ); }
Next, we create our implementation class. In addition to implementing the interface methods from ICBMModule, we must also implement methods from the Module and CopyFrom interfaces:
package javaxml3.icbm; public class ICBMModuleImpl implements ICBMModule { private float latitude; private float longitude; // ICBMModule interface methods public float getLatitude( ) { return latitude; } public float getLongitude( ) { return longitude; } public void setLatitude(float latitude) { this.latitude = latitude; } public void setLongitude(float longtitude) { this.longitude = longtitude; } // Module interface methods public Object clone( ) { ICBMModuleImpl clone = new ICBMModuleImpl( ); clone.copyFrom(this); return clone; } public String getUri( ) { return URI; } // CopyFrom interface methods public void copyFrom(Object obj) { ICBMModuleImpl icbm = (ICBMModuleImpl) obj; latitude = icbm.latitude; longitude = icbm.longitude; } public Class getInterface( ) { return ICBMModule.class; } }
To implement the ModuleParser interface, it's necessary to create a method named parse( ) that accepts a JDOM Element object as input and returns a module object. If the module object cannot be created, the parse( ) method should return null. In the case of the ICBM module, we want to ensure that both latitude and longitude are specified, so we only return an instance of ICBMModuleImpl if both elements are included:
package javaxml3.icbm.io; import javaxml3.icbm.ICBMModule; import javaxml3.icbm.ICBMModuleImpl; import org.jdom.Element; import org.jdom.Namespace; import com.sun.syndication.feed.module.Module; import com.sun.syndication.io.ModuleParser; public class ICBMParser implements ModuleParser { private static final Namespace NS = Namespace.getNamespace(ICBMModule.URI); public String getNamespaceUri( ) { return ICBMModule.URI; } public Module parse(Element element) { boolean foundSomething = false; ICBMModuleImpl icbm = new ICBMModuleImpl( ); Element e = element.getChild("latitude", NS); if (e != null) { try { icbm.setLatitude(Float.parseFloat(e.getText( ))); foundSomething = true; } catch (NumberFormatException ex) { // do nothing } } if (foundSomething) { e = element.getChild("longitude", NS); if (e != null) { try { icbm.setLongitude(Float.parseFloat(e.getText( ))); } catch (NumberFormatException ex) { // can't have a latitude without a longtitude foundSomething = false; } } else { // can't have a latitude without a longtitude foundSomething = false; } } return (foundSomething) ? icbm : null; } }
Our generator class is responsible for adding appropriate elements to a JDOM Element object. In addition, the getNamespaces( ) method can return a Set of JDOM Namespace objects to be added to the output document's root element.
package javaxml3.icbm.io; import java.util.Collections; import java.util.Set; import javaxml3.icbm.ICBMModule; import org.jdom.Element; import org.jdom.Namespace; import com.sun.syndication.feed.module.Module; import com.sun.syndication.io.ModuleGenerator; public class ICBMGenerator implements ModuleGenerator { private static final Namespace ICBM_NS = Namespace.getNamespace( ICBMModule.PREFIX, ICBMModule.URI); private static final Set NS_SET = Collections.singleton(ICBM_NS); public void generate(Module module, Element element) { ICBMModule icbm = (ICBMModule) mm odule; Element latitude = new Element("latitude", ICBM_NS); latitude.setText(Float.toString(icbm.getLatitude( ))); element.addContent(latitude); Element longitude = new Element("longitude", ICBM_NS); longitude.setText(Float.toString(icbm.getLongitude( ))); element.addContent(longitude); } public Set getNamespaces( ) { return NS_SET; } public String getNamespaceUri( ) { return ICBMModule.URI; } }
Finally, we need a rome.properties file to register our parser and generator. We'll tell ROME that the module is usable with RSS 1.0 and RSS 2.0:
rss_2.0.item.ModuleParser.classes=javaxml3.icbm.io.ICBMParser rss_1.0.item.ModuleParser.classes=javaxml3.icbm.io.ICBMParser rss_2.0.item.ModuleGenerator.classes=javaxml3.icbm.io.ICBMGenerator rss_1.0.item.ModuleGenerator.classes=javaxml3.icbm.io.ICBMGenerator
From here, all that needs to be done is to create a JAR file containing these classes and properties file. I'll leave that up to you to create (or you can just download it from this tutorial's web site at http://www.oracle.com/catalog/9780596101497). With that JAR file, our module is complete and ROME will be able to create instances of ICBMParser and ICBMGenerator, invoke the getNamespaceUri( ) method to discover the namespace these classes apply to, and then class the appropriate interface method when necessary. I'll leave it up to you to package these classes and the properties file in a JAR file. Keep in mind that the rome.properties file must be in the root directory of the JAR file. The extensibility of ROME and its extensive support for the various flavors of RSS (which is a by-product of the library's extensibility) make it a compelling choice if you need to write Java code that reads and/or writes RSS and Atom feeds. It is not alone. Informa, available at http://informa.sf.net, is another popular library and there are many others. Although RSS is simple enough that you could just use a generic XML library such as SAX or DOM, the wide availability of these RSS-specific libraries should inspire you to check out one or two others before writing your own RSS parsing or generating code.