Groovy + Java = Scripting++
jueves 10/09/2009
Internet es una fuente inagotable de información. Tanto, que es imposible estar al tanto de todo lo que se publica sin ayuda, y esa ayuda tiene normalmente forma de herramientas de agregación y/u organización como BlogLines. En el ámbito de Groovy, tenemos dos magníficos sitios que agregan gran parte de la información que se publica a diario sobre nuestro lenguaje favorito: GroovyBlogs.org, dedicado a agragar feeds RSS, y GroovyTweets.org, que muestra los tweets de personas y grupos más o menos relevantes en el ámbito de Groovy.Además, tenemos las listas de correo. Si somos suficientemente constantes para seguirlas a diario, nos mantendrán informados sobre el día a día de los desarrollos y las incidencias de proyectos como Groovy y Grails. Recientemente, además, se ha presentado gr8forums.org, un sitio de foros al estilo tradicional sobre Groovy, Grails, Griffon, y demás tecnologías relacionadas.
Como veréis, las fuentes de información empiezan a aumentar en volumen, y disparidad, y estar al tanto de todo lo que ocurre puede llegar a robar mucho tiempo al día. Por eso vamos a aprovechar esta circunstancia para hablar de las posibilidades de Groovy como lenguaje de scripting y de su capacidad para tratar distintas fuentes de información. Vamos a desarrollar un script que:
- Definirá una serie de fuentes de información.
- Tratará cada fuente de forma específica, ya sea leyendo un feed RSS o procesando HTML de una web.
- Compondrá un resumen con la información obtenida, a partir de una plantilla definida previamente.
- Enviará por FTP el archivo generado a un servidor web.
Una vez terminado el script, lo único que tendremos que hacer será programar una tarea automática (cron, si estamos en unix) que lo ejecute a intervalos de tiempo para que el visitante siempre tenga una referencia de lo último que se está publicando.
Y ya que nos ponemos, reservamos un dominio y lo presentamos ante todos vosotros: AllAboutGroovy.com
Requisitos previos
Antes de empezar, hay algunas librerías que debemos colocar en la carpeta $HOME/.groovy/lib para que todo funcione:
La parte principal del script
La filosofía de este script es la siguiente: existe una lista de recursos a procesar, y para recurso hay una función que hace el trabajo específico (leer un feed rss, o procesar html con una estructura particular) y devuelve un fragmento de html. Una vez obtenido, se sustituyen todos los fragmentos en una plantilla html que podéis ver aquí.
Esta es la parte fundamental:
import java.util.regex.Matcher import java.util.regex.Pattern import java.text.DateFormat import org.apache.commons.net.ftp.FTPClient def fNames = [ 'Timestamp', 'GroovyBlogs', 'GroovyTweets', 'GroovyMailList', 'GrailsMailList', 'GR8Forums' ] def fragments = [:] //XMLSlurper form HTML parsing def htmlSlurp = new XmlSlurper(new org.cyberneko.html.parsers.SAXParser()) //XMLSlurper form XML parsing def xmlSlurp = new XmlSlurper() def fragment def t0 /* =========================================================================================== 1. Obtain information and generate the model. ============================================================================================== */ fNames.each{fName -> t0 = System.currentTimeMillis() println "** $fName" try{ fragment = "parse$fName"(htmlSlurp, xmlSlurp) fragments."$fName" = fragment }catch(Exception x){ fragments."$fName" = x.message } printTS(t0) } /* =========================================================================================== 2. parse the Template and generate index.html ============================================================================================== */ def idx = new File("web/index.html") def tpl = new File("web/index.tpl.html").text idx.delete() fragments.each{k,v-> //println "Reemplazando $k por $v" tpl = tpl.replaceAll("##$k##",Matcher.quoteReplacement(v)) } idx.write(tpl) /* =========================================================================================== 3. Upload index.html to web server. ============================================================================================== */ def ftpServer = "www.allaboutgroovy.com" def ftpUser = "allaboutgroovy" def ftpPass = "4llab0utgr00vy" def ftpClient = new FTPClient() ftpClient.connect( ftpServer ) ftpClient.login( ftpUser, ftpPass ) println "Connected to $ftpServer. $ftpClient.replyString" ftpClient.changeWorkingDirectory( './httpdocs' ) println "Directory changed. $ftpClient.replyString" ftpClient.enterLocalPassiveMode() idx.withInputStream{ fis -> ftpClient.storeFile( idx.name, fis ) } println "Upload completed: $ftpClient.replyString" ftpClient.logout() ftpClient.disconnect()
Las funciones de procesamiento
/* =========================================================================================== ++ Auxiliary functions to parse each individual information source ++ ============================================================================================== */ /* parse GR8Forums (html) */ def parseGR8Forums(htmlSlurp, xmlSlurp){ def u = 'http://gr8forums.org/search.php?search_id=active_topics' println "\tProcesando $u" def html = htmlSlurp.parse(u) def result = '
- '
def elements = html.'**'.findAll{it.name()=='A' && it.@class.toString().equals('topictitle')}
println "\tEnlaces encontrados: ${elements.size()}"
def i = 0
elements.each{
result += "
- ${it.text().trim()} " } result += '
- '
def i = 0
println "\tLeyendo ${feed.entry.size()} elementos..."
feed.entry.each{
result += "
- ${it.title} " } result += '
- '
def i = 0
println "\tLeyendo ${feed.entry.size()} elementos..."
feed.entry.each{
result += "
- ${it.title} " } result += '
- '
def elements = html.'**'.findAll{it.name()=='A' && it.@href.toString().contains('/jump/') && it.text()}
println "\tEnlaces encontrados: ${elements.size()}"
def i = 0
elements.each{
result += "
- ${it.text().trim()} " } result += '
- '
def elements = html.'**'.findAll{it.name()=='DIV' && it.@id.toString().startsWith('tweet')}
println "\tTweets encontrados: ${elements.size()}"
def name, status, links, nodes
def i = 0
elements.each{
name = it.'**'.findAll{it.name()=='A' && it.@class == 'screenNameLink'}[0]
name = "${name.text().trim()}"
status = it.'**'.findAll{it.name()=='SPAN' && it.@class == 'statusText'}[0]
/*
GroovyTweets uses EasyThumb to embed thumbs in every link. if the tweet has no links
we can just take status.text(). if it has one or more links, we have to parse it more
deeply to remove the spans.
*/
nodes = status.'**'.findAll{it.name()=='SPAN' && it.@class=='thumb'}
nodes.each{thumb->
thumb.replaceBody("")
}
status = replaceLinks(status.text())
status = replaceTwitterSearches(status)
status = replaceTwitterUsers(status)
result += "
- $name: ${status} " } result +='
Funciones de utilidad
/* =========================================================================================== ++ Utilities ++ ============================================================================================== */ def printTS(t0){ def t = System.currentTimeMillis() - t0 println "$t ms." } def replaceTwitterSearches(input){ def s = findTwitterSearches(input) s.each{ input = input.replaceAll(it[0..it.size()-1],"$it") } return input } def replaceTwitterUsers(input){ def s = findTwitterUsers(input) s.each{ input = input.replaceAll(it[0..it.size()-1],"$it") } return input } def replaceLinks(input){ def urls = findUrls(input) urls.each{ input = input.replaceAll(it,"$it") } return input } def findUrls(text) { def URL_PATTERN = Pattern.compile("\\b(https?|ftp|file)://[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|]",Pattern.CASE_INSENSITIVE) def links = applyPattern(URL_PATTERN, text) return links; } def findTwitterSearches(text){ def pattern = Pattern.compile("\\#\\w+\\W",Pattern.CASE_INSENSITIVE) def items = applyPattern(pattern, text) return items; } def findTwitterUsers(text){ def pattern = Pattern.compile("@\\w+\\s",Pattern.CASE_INSENSITIVE) def items = applyPattern(pattern, text) return items; } def applyPattern(pat, text){ Matcher m = pat.matcher(text); def items = [] while (m.find()) { items.add(m.group()); } return items }
Podéis ver el script completo aquí. Como véis, no se trata de un script muy grande, y permite unificar lo mejor de dos mundos: el de la automatización de tareas en el servidor y las librerías existentes para la plataforma Java.