This tutorial walks through the steps of creating a custom module for syndication feeds that support additional namespaces (such as RSS 1.0, RSS 2.0 and Atom 0.3).
To understand this tutorial you should be familiar the with Rome API and with the use of modules in syndication feeds.
Out of the box Rome parsers and generators support plug-ability of modules for RSS 1.0, RSS 2.0 and Atom 0.3 at both feed and item/entry levels. Also support for the Dublin Core and Syndication modules is provided.
The complete source for this tutorial is in the Rome samples bundle as well as in CVS.
The goal is to add support for a hypothetical Sample Module by defining a module bean, module parser and module generator and the necessary configuration to wire the parser and generator to an RSS 1.0 parser and RSS 1.0 generator.
The sample module defines 3 elements, 'bar', 'foo' and 'date', where 'bar' and 'date' may occur at most once and the 'foo' element may occur several times. For example, a feed with the Sample Module data would look like:
<?xml version="1.0"?> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:sample="http://rome.dev.java.net/module/sample/1.0"> <channel> <title>RSS 1.0 Feed with Sample Module</title> <link>http://rome.dev.java.net</link> <description>This is a feed showing how to use a custom module with Rome</description> <items> <rdf:Seq> <rdf:li resource="item01" /> <rdf:li resource="item02" /> </rdf:Seq> </items> <sample:bar>Channel bar</sample:bar> <sample:foo>Channel first foo</sample:foo> <sample:foo>Channel second foo</sample:foo> </channel> <item rdf:about="item01"> <title>Title of Item 01</title> <link>http://rome.dev.java.net/item01</link> <description>Item 01 does not have Sample module data</description> </item> <item rdf:about="item02"> <title>Title of Item 02</title> <link>http://rome.dev.java.net/item02</link> <description>Item 02 has Sample module data</description> <sample:bar>Item 02 bar</sample:bar> <sample:foo>Item 02 only foo</sample:foo> <sample:date>2004-07-27T00:00+00:00</sample:date> </item> </rdf:RDF>
First we must start with the bean interface, SampleModuleI. SampleModuleI must extend ModuleI. The ModuleI interface defines the getUri() method, which will return a URI identifying the module. The ModuleI interface also extends the Cloneable, ToString and CopyFrom interfaces to ensure that the beans provide support for basic features as cloning, equality, toString, etc. (for more details on this check the Understanding the Rome common classes and interfaces document).
The SampleModule's URI is defined as a constant, accessible via the getURI() instance method. This is for convenience and good practice only.
Then the module defines 3 properties: bar (String), foos (List) and date (Date). The elements of the foos property list must be strings (wishing for Java 5 generics already?).
public interface SampleModuleI extends ModuleI,CopyFrom { public static final String URI = "http://rome.dev.java.net/module/sample/1.0"; public String getBar(); public void setBar(String bar); public List getFoos(); public void setFoos(List foos); public Date getDate(); public void setDate(Date date); }
Next we have to write the bean implementation, SampleModule.
SampleModule extends Module (the implementation counterpart of ModuleI). Module extends ObjectBean which provides equals, hashCode, toString and clone support for the properties of the class given in the constructor (SampleModuleI). Also the URI of the Sample module is indicated; it will be used by the Module.getUri() method.
public class SampleModule extends Module implements SampleModuleI { ... public SampleModule() { super(SampleModuleI.class,SampleModuleI.URI); } public String getBar() { return _bar; } ...
The module properties are just Java Bean properties. The only catch is to follow Rome semantics for Collection properties: in the case of a null value for the collection, an empty collection must be returned.. Also, following Rome semantics, all properties are by reference, including mutable ones.
public class SampleModule extends Module implements SampleModuleI { private String _bar; private List _foos; private Date _date; ... public void setBar(String bar) { _bar = bar; } public List getFoos() { return (_foos==null) ? (_foos=new ArrayList()) : _foos; } public void setFoos(List foos) { _foos = foos; } public Date getDate() { return _date; } public void setDate(Date date) { _date = date; }
Now the weird part: the bits for the CopyFrom logic. The Understanding the Rome common classes and interfaces document fully explains the CopyFrom logic in detail. In short, the CopyFrom interface is to support copying properties from one implementation of a bean (defined through an interface) to another implementation of the same bean.
The getInterface() method returns the bean interface of the implementation (this is necessary for collections containing sub-classes such as a list of modules).
The copyFrom() method copies all the properties from the parameter object (which must be a SampleModuleI implementation) into the caller bean properties. Note that the copyFrom must do a deep copy.
public class SampleModule extends Module implements SampleModuleI { ... public Class getInterface() { return SampleModuleI.class; } public void copyFrom(Object obj) { SampleModuleI sm = (SampleModuleI) obj; setBar(sm.getBar()); List foos = new ArrayList(sm.getFoos()); // this is enough for the copy because the list elements are inmutable (Strings) setFoos(foos); setDate((Date)sm.getDate().clone()); // because Date is not inmutable. } }
The sample module parser must implement the ModuleParser interface. This interface defines 2 methods, getNamespaceUri() that returns the URI of the module and parse(Element) which extracts the module elements from the given Element.
The feed parsers will invoke the module parser with a feed element or with an item element. The module parser must look for module elements in the children of the given element. That is was the Sample parser is doing when looking for 'bar', 'foo' and 'date' children elements in the received element.
In the case of the 'foo' element it looks for all occurrences as the Sample module schema allows for more than one occurrence of the 'foo' element. A SampleModule bean is created and the found values are set into it. For the 'date' element it assumes its a date value in W3C datetime format.
If no Sample Module elements are found in the feed, the parse(Element) method returns null. This is to avoid having an empty instance of a module -not present in the feed- in the feed bean or in the item bean.
public class SampleModuleParser implements ModuleParser { private static final Namespace SAMPLE_NS = Namespace.getNamespace("sample", SampleModuleI.URI); public String getNamespaceUri() { return SampleModuleI.URI; } public ModuleI parse(Element dcRoot) { boolean foundSomething = false; SampleModuleI fm = new SampleModule(); Element e = dcRoot.getChild("bar", SAMPLE_NS); if (e != null) { foundSomething = true; fm.setBar(e.getText()); } List eList = dcRoot.getChildren("foo", SAMPLE_NS); if (eList.size() > 0) { foundSomething = true; fm.setFoos(parseFoos(eList)); } e = dcRoot.getChild("date", SAMPLE_NS); if (e != null) { foundSomething = true; fm.setDate(DateParser.parseW3CDateTime(e.getText())); } return (foundSomething) ? fm : null; } private List parseFoos(List eList) { List foos = new ArrayList(); for (int i = 0; i < eList.size(); i++) { Element e = (Element) eList.get(i); foos.add(e.getText()); } return foos; } }
The sample module generator must implement the ModuleGenerator interface. This interface defines 2 methods, getNamespaceUri() that returns the URI of the module and generate(ModuleI,Element) which injects the module data into the given Element.
The feed generator will invoke the module generator with a feed element or with an item element. The module generator must inject the module properties into the given element (which is a feed or an item). This injection has to be done using the right namespace.
If no Sample Module bean is in the feed bean the module generator is not invoked at all.
public class SampleModuleGenerator implements ModuleGenerator { private static final Namespace SAMPLE_NS = Namespace.getNamespace("sample", SampleModuleI.URI); public String getNamespaceUri() { return SampleModuleI.URI; } public void generate(ModuleI module, Element element) { // this is not necessary, it is done to avoid the namespace definition in every item. Element root = element; while (root.getParent()!=null && root.getParent() instanceof Element) { root = (Element) element.getParent(); } root.addNamespaceDeclaration(SAMPLE_NS); SampleModuleI fm = (SampleModuleI)module; if (fm.getBar() != null) { element.addContent(generateSimpleElement("bar", fm.getBar())); } List foos = fm.getFoos(); for (int i = 0; i < foos.size(); i++) { element.addContent(generateSimpleElement("foo",foos.get(i).toString())); } if (fm.getDate() != null) { element.addContent(generateSimpleElement("date", DateParser.parseW3CDateTime(fm.getDate()))); } } protected Element generateSimpleElement(String name, String value) { Element element = new Element(name, SAMPLE_NS); element.addContent(value); return element; } }
The last step is to setup the configuration file to indicate to Rome how and when to use the Sample Module parser and generator.
The configuration is stored in a Properties file, 'rome.properties', that has to be in the root of the classpath or JAR where the Sample Module bean, parser and generator classes are.
You must indicate the syndication feed formats (ie RSS 1.0) that must be aware of the Sample Module. You must indicate if the Sample Module is available for feed or item elements, or for both. You must indicate both the parser and the generator classes.
Following is the 'rome.properties' file for the Sample Module, it's defined for RSS 1.0 only, for both feed and item elements.
# Parsers for RSS 1.0 feed modules # rss_1.0.feed.ModuleParser.classes=com.rometools.rome.samples.module.SampleModuleParser # Parsers for RSS 1.0 item modules # rss_1.0.item.ModuleParser.classes=com.rometools.rome.samples.module.SampleModuleParser # Generators for RSS 1.0 feed modules # rss_1.0.feed.ModuleGenerator.classes=com.rometools.rome.samples.module.SampleModuleGenerator # Generators for RSS_1.0 entry modules # rss_1.0.item.ModuleGenerator.classes=com.rometools.rome.samples.module.SampleModuleGenerator
If you are defining more than one module, indicate the parser and generator implementation classes separated by commas or spaces.
They will be there, just use them. You may get the SampleModuleI bean using the getModule(String Uri) method of the SyndFeedI bean and the SyndEntryI bean.
Adding or replacing a syndication feed parser, generator or converter is along the same lines of what it has been explained in the tutorial for modules. Eventually we'll have a doc on that too