Friday, October 19, 2007

Extending ROME to do RSS 2.0

In my previous post, I mentioned that I was trying to use ROME to convert an existing RSS 2.0 feed. ROME provides an abstraction of the various RSS and Atom flavors using its SyndFeed object. A Java programmer using the ROME library would use the SyndFeed object to build up a feed, then write it out into a WireFeed object of the appropriate type, using code like the one shown below:

1
2
3
4
5
  SyndFeed myfeed = new SyndFeedImpl();
  ... // populate the feed object
  WireFeedOutput outputter = new WireFeedOutput();
  WireFeed wirefeed = myfeed.createWireFeed("rss_2.0");
  System.out.println(outputter.output(wirefeed));

My problem was that I had a //rss/channel/item/source tag in the feed I was trying to convert. There did not seem to be any way to set anything in the SyndEntryImpl object (the ROME abstraction of the RSS item and the Atom entry elements) that would translate to a RSS item/source element. This seems strange, since the source element is valid RSS 2.0 according to the RSS 2.0 specifications. It may just be an oversight or a conscious decision by the ROME developers to exclude this element, but I needed it. Luckily, ROME was designed with extension in mind, and with the help of Dave Johnson's RAIA book, it was pretty simple to do. This post describes what I needed to do so that I could set a source attribute into the SyndEntryImpl object which would render and parse to and from a RSS 2.0 WireFeed object.

I chose the SyndEntry.setContributors() method which takes a List of SyndPerson objects as the method to set my source attribute from the SyndEntry object. The contributors object is used in Atom, so since I was not going to use Atom in this particular case, it was safe to use this method.

First I built a custom converter by extending the built in RSS 2.0 converter. The job of the converter is to translate between a SyndFeed and an RSS or Atom Feed object. However, since all I wanted to do was to be able to push a source object into my SyndEntry object and get it out when it was converted to an Item object, I extended protected methods of the superclass that does just that. Here is the code for my converter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// ConverterForMyRss20.java
package com.mycompany.myapp.mymodule;

import java.util.Arrays;
import java.util.List;

import com.sun.syndication.feed.rss.Item;
import com.sun.syndication.feed.rss.Source;
import com.sun.syndication.feed.synd.Converter;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndPerson;
import com.sun.syndication.feed.synd.SyndPersonImpl;
import com.sun.syndication.feed.synd.impl.ConverterForRSS20;

public class ConverterForMyRss20 extends ConverterForRSS20 {

  /**
   * Default Ctor. Must be implemented and delegates to type ctor.
   */
  public ConverterForMyRss20() {
    this("myrss_2.0");
  }

  /**
   * Type Ctor. Must be implemented and delegates to superclass type ctor.
   * @param feedType the feed type to convert.
   */
  public ConverterForMyRss20(String feedType) {
    super(feedType);
  }
  
  /**
   * Called from {@link Converter#createRealFeed(com.sun.syndication.feed.synd.SyndFeed)}
   * Delegates to the superclass to create a partially populated Item object, then
   * provides additional logic to pull extra elements from the SyndEntry object into
   * the Item object and returns it.
   * @param syndEntry the SyndEntry to populate from.
   * @return the Item object.
   */
  @Override
  @SuppressWarnings("unchecked")
  protected Item createRSSItem(SyndEntry syndEntry) {
    Item item = super.createRSSItem(syndEntry);
    List<SyndPerson> contributors = syndEntry.getContributors();
    if (contributors != null && contributors.size() > 0) {
      Source source = new Source();
      source.setValue(contributors.get(0).getName());
      item.setSource(source);
    }
    return item;
  }
  
  /**
   * Called from {@link Converter#copyInto(com.sun.syndication.feed.WireFeed, com.sun.syndication.feed.synd.SyndFeed)}
   * Delegates to the superclass to create a partially populated SyndEntry object, then
   * adds the extra elements from the Item object to the SyndEntry object.
   * @param item the Item object to convert to a SyndEntry object.
   */
  protected SyndEntry createSyndEntry(Item item) {
    SyndEntry syndEntry = super.createSyndEntry(item);
    Source source = item.getSource();
    if (source != null) {
      SyndPerson syndPerson = new SyndPersonImpl();
      syndPerson.setName(source.getValue());
      syndEntry.setContributors(Arrays.asList(new SyndPerson[] {syndPerson}));
    }
    return syndEntry;
  }
}

I also needed to write a custom WireFeedParser and WireFeedGenerator to parse RSS 2.0 feeds to and from ROME objects. I really only needed to build the generator, but I built the parser too, just in case I need to provide my clients with tools to parse the RSS feeds I generate. Here is the code for my custom WireFeed parser and generator. As before, I override the RSS 2.0 parser and generator, so I only need to override a single protected method in each superclass.

Here is the code for my custom WireFeed parser. As in the case of the custom converter, the default and type constructors are required, and class just overrides the parseItem() method in the built in WireFeed parser for RSS 2.0 to grab the source JDOM Element from the XML and add it to the RSS Item object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// MyRss20Parser.java
package com.mycompany.myapps.mymodule;

import org.jdom.Element;

import com.sun.syndication.feed.rss.Item;
import com.sun.syndication.feed.rss.Source;
import com.sun.syndication.io.impl.RSS20Parser;

public class MyRss20Parser extends RSS20Parser {
  
  /**
   * Default Ctor. Must be implemented and delegates to type ctor.
   */
  public MyRss20Parser() {
    this("myrss_2.0");
  }
  
  /**
   * Type Ctor. Must be implemented and delegates to superclass type ctor.
   * @param feedType the feed type for this parser.
   */
  public MyRss20Parser(String feedType) {
    super(feedType);
  }
  
  /**
   * Called from {@link com.sun.syndication.io.WireFeedParser#parse(org.jdom.Document, boolean)}
   * Delegates to the superclass to build an Item object, then adds the source to the
   * returned Item if it exists in the Element itemElement.
   * @param rssRoot the root Element of the RSS feed.
   * @param itemElement the Element representing the Item object.
   * @return the Item object with the source added if it exists.
   */
  @Override
  public Item parseItem(Element rssRoot, Element itemElement) {
    Item item = super.parseItem(rssRoot, itemElement);
    Element sourceElement = itemElement.getChild("source", getRSSNamespace());
    if (sourceElement != null) {
      Source source = new Source();
      source.setValue(sourceElement.getTextTrim());
      item.setSource(source);
    }
    return item;
  }
}

And here is the code for the custom WireFeedGenerator. As before, the default and the type constructors are required by the framework, and all this class does is to override the superclass's populateItem() method, to populate an Item JDOM Element object with the source Element if the Item.getSource() value is not null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// MyRss20Generator.java
package com.mycompany.myapp.mymodule;

import org.jdom.Element;

import com.sun.syndication.feed.rss.Item;
import com.sun.syndication.io.WireFeedGenerator;
import com.sun.syndication.io.impl.RSS20Generator;

public class MyRss20Generator extends RSS20Generator {

  /**
   * Default Ctor. Must be implemented and delegates to type ctor.
   */
  public MyRss20Generator() {
    this("myrss_2.0", "2.0");
  }
  
  /**
   * Type Ctor. Must be implemented and delegates to superclass type ctor.
   * @param feedType the feed type for this generator.
   * @param version the feed version for this generator.
   */
  public MyRss20Generator(String feedType, String version) {
    super(feedType, version);
  }
  
  /**
   * Called from {@link WireFeedGenerator#generate(com.sun.syndication.feed.WireFeed)}
   * Delegates to the superclass to partially populate an Item element from
   * an Item object. Adds on a source element to the item element if the Item
   * object has a non-null source.
   * @param item the Item object from which to populate.
   * @param itemElement the Element being populated from the Item.
   * @param index the index of the object, not used here.
   */
  @Override
  public void populateItem(Item item, Element itemElement, int index) {
    super.populateItem(item, itemElement, index);
    if (item.getSource() != null) {
      Element sourceElement = new Element("source", getFeedNamespace());
      sourceElement.setText(item.getSource().getValue());
      itemElement.addContent(sourceElement);
    }
  }
}

To let ROME know that I will be using my dialect of RSS 2.0 (myrss_2.0) which supports the source element, I need to update the rome.properties file with the mapping to the above classes. I also need to change the mapping for the Module parser and generator to use this dialect. My complete rome.properties looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
myrss_2.0.item.ModuleParser.classes=\
com.mycompany.myapp.mymodule.MyModuleParser

myrss_2.0.item.ModuleGenerator.classes=\
com.mycompany.myapp.mymodule.MyModuleGenerator

WireFeedParser.classes=\
com.mycompany.myapp.mymodule.MyRss20Parser

WireFeedGenerator.classes=\
com.mycompany.myapp.mymodule.MyRss20Generator

Converter.classes=\
com.mycompany.myapp.mymodule.ConverterForMyRss20

Once this is all done, the Java code to set a source element inside a SyndEntry object looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  SyndEntry entry = new SyndEntryImpl();
  ...
  SyndPerson source = new SyndPersonImpl();
  source.setName(mydomainObject.getSourceName());
  entry.setContributors(Arrays.asList(new SyndPerson[] {source}));
  ...
  feed.getEntries().add(entry);

  WireFeedOutput outputter = new WireFeedOutput();
  WireFeed wirefeed = myfeed.createWireFeed("myrss_2.0");
  System.out.println(outputter.output(wirefeed));

And I am happy to say that the resulting item elements in the RSS 2.0 feed did contain the source as expected. However, it is very possible that I am doing something wrong and there is some settable field in SyndEntry that will allow the source to be populated without going through all this trouble. If there is, I would appreciate being corrected.

Update

I found that once I put in my custom WireFeed generator and parser, and the custom converter to convert between SyndEntry and Item objects, some of my old feeds began to fail. Specifically, it was not generating the OpenSearch element within the channel, even though they were being set in the code. Prior to the change, the OpenSearch elements were showing up fine. I also noticed that some namespace declarations were not showing up. The upshot was that I ended up changing my rome.properties file to explicitly declare all the properties for RSS 2.0 classes in addition to my custom myrss_2.0 dialect. Here is my complete rome.properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# Copied from the base rome.properties for rss_2.0 since we are now using
# our own dialect of rss_2.0
myrss_2.0.feed.ModuleGenerator.classes=\
  com.sun.syndication.io.impl.DCModuleGenerator \
  com.sun.syndication.io.impl.SyModuleGenerator \
  com.sun.syndication.feed.module.opensearch.impl.OpenSearchModuleGenerator

myrss_2.0.feed.ModuleParser.classes=\
  com.sun.syndication.io.impl.DCModuleParser \
  com.sun.syndication.io.impl.SyModuleParser \
  com.sun.syndication.feed.module.opensearch.impl.OpenSearchModuleParser

myrss_2.0.item.ModuleParser.classes=\
  com.mycompany.myapp.mymodule.MyModuleParser

myrss_2.0.item.ModuleGenerator.classes=\
  com.mycompany.myapp.mymodule.MyModuleGenerator

WireFeedParser.classes=\
  com.sun.syndication.io.impl.RSS090Parser \
  com.sun.syndication.io.impl.RSS091NetscapeParser \
  com.sun.syndication.io.impl.RSS091UserlandParser \
  com.sun.syndication.io.impl.RSS092Parser \
  com.sun.syndication.io.impl.RSS093Parser \
  com.sun.syndication.io.impl.RSS094Parser \
  com.sun.syndication.io.impl.RSS10Parser  \
  com.sun.syndication.io.impl.RSS20wNSParser  \
  com.sun.syndication.io.impl.RSS20Parser  \
  com.sun.syndication.io.impl.Atom10Parser \
  com.sun.syndication.io.impl.Atom03Parser \
  com.mycompany.myapp.mymodule.MyRss20Parser

WireFeedGenerator.classes=\
  com.sun.syndication.io.impl.RSS090Generator \
  com.sun.syndication.io.impl.RSS091NetscapeGenerator \
  com.sun.syndication.io.impl.RSS091UserlandGenerator \
  com.sun.syndication.io.impl.RSS092Generator \
  com.sun.syndication.io.impl.RSS093Generator \
  com.sun.syndication.io.impl.RSS094Generator \
  com.sun.syndication.io.impl.RSS10Generator  \
  com.sun.syndication.io.impl.RSS20Generator  \
  com.sun.syndication.io.impl.Atom10Generator \
  com.sun.syndication.io.impl.Atom03Generator \
  com.mycompany.myapp.mymodule.MyRss20Generator

Converter.classes=\
  com.sun.syndication.feed.synd.impl.ConverterForAtom10 \
  com.sun.syndication.feed.synd.impl.ConverterForAtom03 \
  com.sun.syndication.feed.synd.impl.ConverterForRSS090 \
  com.sun.syndication.feed.synd.impl.ConverterForRSS091Netscape \
  com.sun.syndication.feed.synd.impl.ConverterForRSS091Userland \
  com.sun.syndication.feed.synd.impl.ConverterForRSS092 \
  com.sun.syndication.feed.synd.impl.ConverterForRSS093 \
  com.sun.syndication.feed.synd.impl.ConverterForRSS094 \
  com.sun.syndication.feed.synd.impl.ConverterForRSS10  \
  com.sun.syndication.feed.synd.impl.ConverterForRSS20 \
  com.mycompany.myapp.mymodule.ConverterForMyRss20

ROME uses a hierarchy of rome.properties files to configure itself, which works fine when we are adding modules as I did in my last post, but breaks down when we are overriding. Its like overriding a superclass method without calling super.method(), I guess. So we basically have to do the equivalent of the super method call by copying the classes for RSS 2.0 for the overriden properties.

Be the first to comment. Comments are moderated to prevent spam.