Fork me on GitHub

Defining a Custom Module (bean, parser and generator)

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.

What is the intended outcome of the tutorial?

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>

Sample Module Bean

First we must start with the bean interface, SampleModule. SampleModule must extend Module. The Module interface defines the getUri() method, which will return a URI identifying the module.

The Module interface also extends the CopyFrom interface to enable copying properties from alternate implementations of the concrete Module interface. More on this later in this page.

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 SampleModule extends Module {

    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, SampleModuleImpl.

SampleModuleImpl extends ModuleImpl. ModuleImpl is the default implementation of the ==Module interface. ModuleImpl automatically provides equals, hashCode, toString and clone support for the properties of the class given in the constructor (SampleModule). Also the URI of the Sample module is indicated; it will be used by the Module.getUri() method.

public class SampleModuleImpl extends ModuleImpl implements SampleModule {
    ...
    public SampleModuleImpl() {
        super(SampleModule.class,SampleModule.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 SampleModuleImpl extends ModuleImpl implements SampleModule {
    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;
    }

The CopyFrom interface

Now the weird part, the bits for the CopyFrom logic. The The CopyFrom interface (rome) 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 SampleModule implementation) into the caller bean properties. Note that the copyFrom must do a deep copy.

public class SampleModuleImpl extends ModuleImpl implements SampleModule {
    ...
    public Class getInterface() {
        return SampleModule.class;
    }

    public void copyFrom(Object obj) {
        SampleModule sm = (SampleModule) 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.
    }

}

Sample Module Parser

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", SampleModule.URI);

    public String getNamespaceUri() {
        return SampleModule.URI;
    }

    public Module parse(Element dcRoot) {
        boolean foundSomething = false;
        SampleModule fm = new SampleModuleImpl();

        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;
    }

}

Sample Module Generator

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. The set of namespaces returned by the getNamespaces() method will be used to declare the namespaces used by the module in the feed root element.

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", SampleModule.URI);

    public String getNamespaceUri() {
        return SampleModule.URI;
    }

    private static final Set NAMESPACES;

    static {
        Set nss = new HashSet();
        nss.add(SAMPLE_NS);
        NAMESPACES = Collections.unmodifiableSet(nss);
    }

    public Set getNamespaces() {
        return NAMESPACES;
    }

    public void generate(Module 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 = (SampleModule)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.formatW3CDateTime(fm.getDate())));
        }
    }

    protected Element generateSimpleElement(String name, String value)  {
        Element element = new Element(name, SAMPLE_NS);
        element.addContent(value);
        return element;
    }

}

Configuration to make ROME process Sample Module in feeds

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.

Using the Sample Module from the SyndFeed beans

They will be there, just use them. You may get the SampleModule bean using the getModule(String Uri) method of the SyndFeed bean and the SyndEntry bean.

Adding or replacing a syndication feed parser, generator or converter goes along the same lines of what it has been explained in the tutorial for modules. This is explained in the ROME Plugins Mechanism topic.