I’ve updated my prototype to use the excellent Rome feed parser library. Instead of dumping 20kB of ‘useful’ raw feed on you, it now formats the entries nicely.
I’ve hooked it up to deliver me real-time headlines from my Google Reader feed and from TechCrunch, both of which work flawlessly.
With all the building blocks I’ve strung together, this really wasn’t any work at all. All of the complexity lies in the cloud: Google’s AppEngine and XMPP implementation and the PubSubHubbub hub. The rest is done with a feed-parsing library.
Here’s the new code:
package com.grack.pubsubhubbub.xmpp;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.xmpp.JID;
import com.google.appengine.api.xmpp.MessageBuilder;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;
@SuppressWarnings("serial")
public class Subscribe extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setStatus(204);
XMPPService xmpp = XMPPServiceFactory.getXMPPService();
JID jid = new JID(req.getPathInfo().substring(1));
SyndFeedInput input = new SyndFeedInput();
SyndFeed feed;
try {
feed = input.build(new XmlReader(req.getInputStream()));
} catch (IllegalArgumentException e) {
throw new ServletException(e);
} catch (FeedException e) {
xmpp.sendMessage(new MessageBuilder().withBody(
"Feed exception: " + e.toString()).withRecipientJids(jid)
.build());
throw new ServletException(e);
}
@SuppressWarnings("unchecked")
List entries = feed.getEntries();
StringBuilder message = new StringBuilder("Got update: \n");
for (SyndEntry entry : entries) {
message.append(entry.getTitle()).append(": ").append(
entry.getLink()).append('\n');
}
xmpp.sendMessage(new MessageBuilder().withBody(message.toString())
.withRecipientJids(jid).build());
}
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setStatus(200);
resp.setContentType("text/plain");
XMPPService xmpp = XMPPServiceFactory.getXMPPService();
JID jid = new JID(req.getPathInfo().substring(1));
if (req.getParameter("hub.mode").equals("subscribe"))
xmpp.sendMessage(new MessageBuilder().withBody(
"Subscribing to " + req.getParameter("hub.topic"))
.withRecipientJids(jid).build());
else
xmpp.sendMessage(new MessageBuilder().withBody(
"Unsubscribing from " + req.getParameter("hub.topic"))
.withRecipientJids(jid).build());
resp.getOutputStream().print(req.getParameter("hub.challenge"));
resp.getOutputStream().flush();
}
}

this looks like exactly what i’ve been looking for. however i’m struggling with a java.lang.NullPointerException on the call – req.getPathInfo().substring(1)
could you give a bit more detail on how to use this for real newbies please?
That probably means your servlet wasn’t configured like so:
Subscribe
/subscribe/*
Make sure you map the wildcard at the end
Oops… blog escaping fail:
<servlet-mapping>
<servlet-name>Subscribe</servlet-name>
<url-pattern>/subscribe/*</url-pattern>
</servlet-mapping>
ahhh, that’s the one. thank you.
awesome. this works a treat.
had a couple of issues along the way, which i’ll repeat here in case it helps any newbies (hope that’s ok?).
- had to change the line: List entries = feed.getEntries(); to: List entries = feed.getEntries();
- needed to include jdom.jar, and xercres.jar(s) along with rome-1.0.jar.
- changed the default JVM to 1.6.0 (leopard defaulted to 1.5.0 but xercres went bang).
everything else worked as expected. many thanks for this post matt, couldn’t have done it without your help.
i (hearts) pubsubhubbub
Awesome. Let me know if your project is public, I’d love to check it out.
hmm the List entries line got eaten. i’ll try again:
List entries = feed.getEntries(); to: List<SyndEntry> entries = feed.getEntries();
am guessing that’s what happened to your post too?
Oops! Yeah, good catch. Thanks for pointing that out.
Hi,
One question: I have included all the libraries, add them to the build path, it compiles fine. But on run, I get this error,
Do you have any idea what might be causing it?
java.lang.ClassNotFoundException: com.sun.syndication.io.XmlReader
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at com.google.appengine.tools.development.IsolatedAppClassLoader.loadClass(IsolatedAppClassLoader.java:151)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClassInternal(Unknown Source)
at java.lang.Class.getDeclaredConstructors0(Native Method)
at java.lang.Class.privateGetDeclaredConstructors(Unknown Source)
at java.lang.Class.getConstructor0(Unknown Source)
at java.lang.Class.newInstance0(Unknown Source)
at java.lang.Class.newInstance(Unknown Source)
at org.mortbay.jetty.servlet.Holder.newInstance(Holder.java:153)
at org.mortbay.jetty.servlet.ServletHolder.getServlet(ServletHolder.java:339)
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:463)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:121)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:342)
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
at org.mortbay.jetty.Server.handle(Server.java:313)
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:830)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:514)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)