Wednesday, November 14, 2007

Grails + ROME + geoRSS

Grails + ROME + geoRSS

If you are looking to do some RSS/ATOM feeds in Grails you likely are looking at either at the examples that Glen Smith has done (ref: http://blogs.bytecode.com.au/glen/2006/12/22/1166781151213.html ) or to work with the Feeds plugins (ref: http://grails.org/Feeds+Plugin ) by Marc Palmer. Both are excellent places to start.

What I want to talk about there is adding geospatial data to a RSS feed in the Grails framework by building off this work. The above efforts leverage off the ROME package from http://rome.dev.java.net. For geospatial elements there is the GeoRSS Module for ROME by Marc Wick (ref: http://georss.geonames.org ).

Adding this ability to a Grails project is rather easy. Obviously you need to have the Rome and GeoRSS jar files from these referenced sites in your project lib directory. From there you can do something like the following. Please note these examples are based on the work of these previously mentioned individuals and the credit is theirs. I just glued things together.

So likely you will end up with something like this in your controller:

  // some import for this..   (watch me polute my namespace) ;)
import com.sun.syndication.feed.synd.*;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.feed.module.georss.*;
import com.sun.syndication.feed.module.georss.geometries.*;


def supportedFormats = ["rss_0.90", "rss_0.91", "rss_0.92", "rss_0.93", "rss_0.94", "rss_1.0", "rss_2.0", "atom_0.3"]

def rss = {
render(text: getFeed("atom_0.3"), contentType: "text/xml", encoding: "UTF-8")
}

def all = {
def format = params.id
if (supportedFormats.contains(format)) {
render(text: getFeed(format), contentType: "text/xml", encoding: "UTF-8")
} else {
response.sendError(response.SC_FORBIDDEN);
}
}

def getFeed(feedType) {

def ageModels = AgeModel.list()
def jdb = Sql.newInstance('[ WE ARE MAKING SQL CALLS SO THE CONNECT STRING GOES HERE. GORM WOULD BE NICER, BUT NOT AN OPTION FOR ME')

def entries = []
ageModels.each {ageModel ->
List functionData = jdb.rows("select latitude_degrees, longitude_degrees from hole where leg like ${ageModel.leg} and site like ${ageModel.site} and hole like '${ageModel.hole}'")
def locationString = functionData[0][1] + ", " + functionData[0][0]

def desc = new SyndContentImpl(type: "text/plain", value: "Entry for leg " + ageModel.leg + " locate at " + locationString + ageModel.rating + " " + ageModel.status);
def entry = new SyndEntryImpl(title: ageModel.leg + "_" +ageModel.site + ageModel.hole + " - " + ageModel.user,
link: 'http://localhost:8080/janusAmp/rest/lsh/' + ageModel.leg + "/" + ageModel.site+ "/"+ageModel.hole,
publishedDate: ageModel.date, description: desc);

// Select the style of encoding you want to do.
// def geoRSSModule = new W3CGeoModuleImpl()
// def geoRSSModule = new GMLModuleImpl()
def geoRSSModule = new SimpleModuleImpl()
geoRSSModule.setPosition(new Position(functionData[0][0], functionData[0][1]));
entry.getModules().add(geoRSSModule);
entries.add(entry);
}

SyndFeed feed = new SyndFeedImpl(feedType: feedType, title: 'Recently added age models',
link: 'http://www.chronos.org', description: 'List of age models from Janus data',
entries: entries);

StringWriter writer = new StringWriter();
SyndFeedOutput output = new SyndFeedOutput();
output.output(feed, writer);
writer.close();

return writer.toString();
}

The only thing different than what Glen, shows in his blog is the addition of the bold lines to add in the geoRss element. The result... geospatially enabled RSS feeds in the Grails framework (and anywhere else for Groovy and Java of course)

A few things to note:
a) While Google Earth and Google Maps should read this output I had some issues with the resulting files. Running things through the validator at: http://cite.opengeospatial.org/test_engine/georss_validator/ one gets some errors related to the namespaces. I noticed that the examples for RSS feeds at http://georss.org/ even had some issues. While the ATOM version validates ok the RSS based feed errors out in the same way that my generated feeds do. Likely this is an issue with ROME and if I track down the issue I will follow up on it. Until the feeds validate these feeds do not appear to work in Google Earth or Maps.
b) However, you can generate the rss version 2 feed with the SimpleModuleImpl() from the georss jar file and that will translate correctly with the rss to kml style sheet found at http://www.kovacevic.nl/hacks/kml/georss2kml.xsl (ref: http://www.kovacevic.nl/blog ).

RSS to KML via XSL in Grails:

So you can drop into your controller something like:
  def kml = {
def xslPath = getAppPathService.serviceMethod().toString() + "georss2kml.xsl"
def kmlxsl = new File(xslPath).getText().toString()
def rssxml = getFeed("rss_2.0")

def mywriter = new StringWriter()

def factory = TransformerFactory.newInstance()
def transformer = factory.newTransformer(new StreamSource(new StringReader(kmlxsl)))
transformer.transform(new StreamSource(new StringReader(rssxml)), new StreamResult(mywriter))

render(text: mywriter.toString(), contentType: "text/xml", encoding: "UTF-8")
}
you make also need the following in your code for XSL support:
//  for the XSLT transform
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

into your controller where AppPathService is (not mine.. but I don't have the reference for this.. bad me)

 import org.codehaus.groovy.grails.commons.*
import org.apache.commons.logging.*

import org.springframework.beans.BeansException
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware

class GetAppPathService implements ApplicationContextAware {

GrailsApplication grailsApplication
ApplicationContext appCtx

boolean transactional = true

def void setApplicationContext(ApplicationContext arg0) throws BeansException {
appCtx = arg0;
}
def serviceMethod() {
String applicationPath = "${appCtx?.getServletContext()?.getRealPath("/")}"
return applicationPath
}
}
What the KML does is load in the XSL file (it's looking at the root of the response which will be your web-apps directory. You can move and alter this as you wish. Of course you could simple place the XSL in the controller too like at http://groovy.codehaus.org/Processing+XML+with+XSLT which is what kml method is based on. I like this this way a bit better. At this point you should be generating KML from this and Google Earth and Google Maps should load and monitor this URL just fine.

I do want the get the GeoRSS feed vaildating though and will work on that effort as noted.

enjoy
Doug

2 comments:

Graeme Rocher said...

Nice post, make sure you add your blog to http://www.groovyblogs.org so that more of the Groovy community can check it out

Domesticpedia said...

Good post. Thanks for sharing it.
___________________
Hotels in Rome