25 May 2006

First Time with Google-coop

Google Co-op is a platform which enables you to use your expertise to help other users find information(...). With the Google Subscribed Links API, you can add your services directly into Google search. This can help make those services more accessible, giving your users another entry-point to them when they're making a related search on Google.

Today I tested the services from Google Co-op and created a "Subscribed Link" linking my library on connotea. Once a user registers my Subscribed Link, and a connotea tag is requested on google, a link to my library will be displayed.

Using the Connotea web API I downloaded my bookmarks:

wget -O tags.xml "http://login:password@www.connotea.org/data/tags/user/lindenb?num=10000


and the result was transformed with the following xslt stylesheet:
<?xml version='1.0' ?>
<xsl:stylesheet
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:connotea="http://www.connotea.org/2005/01/schema#"
version='1.0'
>
<xsl:output
method='xml'
version="1.0"
encoding="iso-8859-1"
indent="yes"/>

<xsl:param name="username">Name</xsl:param>
<xsl:param name="user">connotea-user</xsl:param>

<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>

<xsl:template match="rdf:RDF">
<Results>
<xsl:apply-templates/>
</Results>
</xsl:template>


<xsl:template match="connotea:TagList">
<xsl:for-each select="connotea:item/connotea:Tag">
<xsl:sort select="connotea:postCount"
data-type="number"
order="descending"
/>
<!-- just keep the 100 first tags with postCount greater than 10 -->
<xsl:if test="position() &lt;= 100 and connotea:postCount &gt;=10">
<xsl:element name="ResultSpec">
<xsl:attribute name="id"><xsl:value-of
select="generate-id()"/></xsl:attribute>
<Query><xsl:value-of select="rdf:value"/></Query>
<Response>
<Output name="title"><xsl:value-of
select="$username"/>&apos;s Connotea Bookmarks</Output>
<Output name="more_url">www.connotea.org/user/<xsl:value-of
select="$user"/>/tag/<xsl:value-of
select="rdf:value"/></Output>
<Output name="text1"><xsl:value-of
select="connotea:postCount"/> bookmarks on www.connotea.org</Output>
</Response>
</xsl:element>
</xsl:if>
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>


xsltproc tags2coop.xsl tags.xml > coop.xml


The coop.xml looks like this:

<?xml version="1.0" encoding="iso-8859-1"?>
<Results
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:connotea="http://www.connotea.org/2005/01/schema#">
<ResultSpec id="id2263072">
<Query>Bioinformatics</Query>
<Response>
<Output name="title">Pierre 's Connotea Bookmarks</Output>
<Output name="more_url">www.connotea.org/user/lindenb/tag/Bioinformatics</Output>
<Output name="text1">334 bookmarks on www.connotea.org</Output>
</Response>
</ResultSpec>
(...)


The file was uploaded at http://www.urbigene.com/coop/coop.xml and registered from http://www.google.com/coop/manage/subscribedlinks.





No when someone register my file and ask google about, say bioinformatics, a link to my library on connotea will appear on the top of the page:



Would be nice if connotea could automaticaly export this xml file for google co-op. I also requested this on the connotea wiki.

I also played with Annotations but I still don't have a clear idea of what it can be used for...

12 May 2006

Playing with Connotea API (3/2 (!) )

Cool stuff can be written quickly. I simply cut and pasted some piece of javascript code I already wrote to create the following GreaseMonkey Script:



A GreaseMonkey Script to Display SVG TreeMaps of Tags in Connotea.
Connotea is a free online reference management service. It allows you to save links to all your favourite articles, references, websites and other online resources with one click. Connotea is also a social bookmarking tool, so you can view other people's collections to discover new, interesting content. The script I wrote is a Greasemonkey user script which alters the content of the web page, when you'browsing connotea: it inserts a treemap of the current tags using SVG. The code was inspired from [here]. As Firefox now supports the SVG format, this drawing can be displayed in your web browser. SVG is a vectorial format: Vector graphics editors allow to rotate, move, mirror, stretch, skew, generally perform affine transformations of objects, change z-order and combine the primitives into more complex objects..

A sample with the tag rotavirus

GMConnoteaSVG


Update 2010-08-12:source code

// pubmed2connotea
// version 0.2 BETA!
// 2006-05-12
// Copyright (c) 2006, Pierre Lindenbaum PhD
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
// http://www.integragen.com
// http://plindenbaum.blogspot.com
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script. To install it, you need
// Greasemonkey 0.6.4 or later: http://greasemonkey.mozdev.org/
// and Firefox 1.5 : http://www.mozilla.com/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "connoteatreemap", and click Uninstall.
//
// --------------------------------------------------------------------

// ==UserScript==
// @name connoteatreemap
// @namespace http://www.integragen.com
// @description showtreemap of tags in SVG when browsing connotea
// @include http://www.connotea.org/recent
// @include http://www.connotea.org/recent?*
// @include http://www.connotea.org/user/*
// @include http://www.connotea.org/tag/*
// @include http://www.connotea.org/date/*
// @include http://www.connotea.org/uri/*
// ==/UserScript==

var Namespaces= new Object();
Namespaces.RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
Namespaces.RDFS ="http://www.w3.org/2000/01/rdf-schema#";
Namespaces.DC="http://purl.org/dc/elements/1.1/";
Namespaces.DCTERMS="http://purl.org/dc/terms/";
Namespaces.PRISM="http://prismstandard.org/namespaces/1.2/basic/";
Namespaces.FOAF="http://xmlns.com/foaf/0.1/";
Namespaces.CONNOTEA="http://www.connotea.org/2005/01/schema#";
Namespaces.XHTML="http://www.w3.org/1999/xhtml";
Namespaces.SVG="http://www.w3.org/2000/svg";
Namespaces.XLINK="http://www.w3.org/1999/xlink";


/**
*
* Color
* eq of java.awt.Color
*
*/
function Color(r,g,b)
{
this.r=r;
this.g=g;
this.b=b;

/* return "rgb(r,b,b)" */
Color.prototype.toSVG=function()
{
return "rgb("+this.r+","+
this.g+","+
this.b+")";
}
}

/**
*
* A Dimension
*
*/
function Dimension(w,h)
{
this.width=w;
this.height=h;

Dimension.prototype.setSize=function(width, height)
{
this.width=width;
this.height=height;
}

Dimension.prototype.setWidth=function(width)
{
this.width=width;
}

Dimension.prototype.setHeight=function(height)
{
this.height=height;
}

Dimension.prototype.getWidth=function()
{
return this.width;
}

Dimension.prototype.getHeight=function()
{
return this.height;
}
}
/**
*
* A Rectangle
*
*/

Rectangle.prototype= new Dimension(0,0);

function Rectangle(x,y,w,h)
{
//
this.x=x;
this.y=y;
this.setSize(w,h);


Rectangle.prototype.setLocation=function(x, y)
{
this.x=x;
this.y=y;
}

Rectangle.prototype.setX=function(x)
{
this.x=x;
}

Rectangle.prototype.setY=function(y)
{
this.y=y;
}



Rectangle.prototype.getX=function()
{
return this.x;
}

Rectangle.prototype.getY=function()
{
return this.y;
}


Rectangle.prototype.setRectangle=function(rect)
{
this.setBounds(
rect.getX(),
rect.getY(),
rect.getWidth(),
rect.getHeight()
);
}

Rectangle.prototype.setBounds=function(x,y,width, height)
{
this.setSize(width, height);
this.setLocation(x,y);
}



Rectangle.prototype.getCenterX=function()
{
return this.getX()+this.getWidth()/2.0;
}

Rectangle.prototype.getCenterY=function()
{
return this.getY()+this.getHeight()/2.0;
}

Rectangle.prototype.toString=function()
{
return "Rectangle("+
this.getX()+","+
this.getY()+","+
this.getWidth()+","+
this.getHeight()+
")";
}
}



/**
*
* A TreeMapItem is a "square" in the TreeMap
* it is initialized with a positive number used as
* the weight of this square
*
*/

TreeMapItem.prototype= new Rectangle(0.0,0.0,0.0,0.0);

function TreeMapItem(weight)
{
//
this.weight=weight;
this.url=null;/* url for hyperlink xml escaped */
this.label=weight;/* label for this item default is weight */
this.fill=null;/* a Color for filling */
this.stroke=new Color(255,255,255);/* a color for stroking */
this.id=null;/* optional id for xml dom */
this.title=null;
//
TreeMapItem.prototype.setFill=function(color)
{
this.fill=color;
}

TreeMapItem.prototype.setID=function(id)
{
this.id=id;
}



TreeMapItem.prototype.getRGBFill=function()
{
if(this.fill==null) return "none";
return this.fill.toSVG();
}

TreeMapItem.prototype.getRGBStroke=function()
{
if(this.stroke==null) return "none";
return this.stroke.toSVG();
}

TreeMapItem.prototype.getWeight=function()
{
return this.weight;
}

TreeMapItem.prototype.setURL=function(url)
{
this.url=url;
}

TreeMapItem.prototype.setTitle=function(title)
{
this.title=title;
}



TreeMapItem.prototype.setLabel=function(label)
{
this.label=label;
}

/* print this item as SVG using stream out and using parameters from its owner treemap */
TreeMapItem.prototype.toSVG=function(treemap,doc)
{
var fontsize= treemap.getFontSize();



var g= doc.createElementNS(Namespaces.SVG,"svg:g");
if(this.id!=null) g.setAttribute("id",this.id);

var node=g;

if(this.url!=null)
{
node= doc.createElementNS(Namespaces.SVG,"svg:a");
var t= this.title;
if(t==null) t=this.url;
g.appendChild(node);
node.setAttributeNS(Namespaces.XLINK,"xlink:title",t);
node.setAttributeNS(Namespaces.XLINK,"xlink:href",this.url);
}

var rect= doc.createElementNS(Namespaces.SVG,"svg:rect");
node.appendChild(rect);
rect.setAttribute("x",this.getX());
rect.setAttribute("y",this.getY());
rect.setAttribute("width",this.getWidth());
rect.setAttribute("height",this.getHeight());
rect.setAttribute("fill",this.getRGBFill());
rect.setAttribute("stroke",this.getRGBStroke());
rect.setAttribute("stroke","black");


if(this.label!=null)
{
while(!( this.getHeight()> fontsize && this.getWidth() > this.label.length*fontsize))
{
--fontsize;
if(fontsize<=4) break;
}
var text= doc.createElementNS(Namespaces.SVG,"svg:text");
text.setAttribute("x",this.getCenterX());
text.setAttribute("y",this.getCenterY());
text.setAttribute("stroke",this.getRGBStroke());
text.setAttribute("font-size",fontsize);
text.appendChild(doc.createTextNode(this.label));
node.appendChild(text);
}
return g;
}

TreeMapItem.prototype.setStroke= function(color)
{
this.stroke=color;
}

}



/* used to sort treeMap Item */
function treemap_item_compare(a,b)
{
if(a.getWeight()== b.getWeight())
{
return 0;
}
else if(a.getWeight()< b.getWeight())
{
return 1;
}
return -1;
}


/**
* The Treemap : a container of TreeMapItem(s)
* contains the treemap algorithm
* original java code from http://www.cs.umd.edu/hcil/treemap-history/Treemaps-Java-Algorithms.zip
* by
* - Martin Wattenberg, w(at)bewitched.com
* - Ben Bederson, bederson(at)cs.umd.edu
* University of Maryland, Human-Computer Interaction Lab
* http://www.cs.umd.edu/hcil
*
*/
TreeMap.prototype= new Dimension(0.0,0.0);
function TreeMap(w,h)
{
this.treemapitems=new Array();
this.fontsize=24;
this.setSize(w,h);


/* add a new TreeMapItem */
TreeMap.prototype.addItem=function(item)
{
if(item.getWeight()<=0) return;
this.treemapitems[this.treemapitems.length]=item;
}

TreeMap.prototype.getFontSize=function()
{
return this.fontsize;
}

TreeMap.prototype.setFontSize=function(fontsize)
{
this.fontsize=fontsize;
}

/* creates the treemap as SVG using doc */
TreeMap.prototype.toSVG=function(doc)
{

var svg = doc.createElementNS(Namespaces.SVG,"svg:svg");
svg.setAttribute("xmlns:svg",Namespaces.SVG);
svg.setAttribute("xmlns:xlink",Namespaces.XLINK);
svg.setAttribute("width",this.getWidth());
svg.setAttribute("height",this.getHeight());
svg.setAttribute("text-anchor","middle");
svg.setAttribute("font-family","monospace");
svg.setAttribute("font-size",this.getFontSize());

var tt= doc.createElementNS(Namespaces.SVG,"svg:title");
svg.appendChild(tt);
tt.appendChild(doc.createTextNode("ConnoteaTreeMap.svg"));

var desc= doc.createElementNS(Namespaces.SVG,"svg:desc");
svg.appendChild(desc);
desc.appendChild(doc.createTextNode("generated by Pierre Lindenbaum PhD 2006 Integragen plindenbaum (at) yahoo (dot) fr http://www.connotea.org/wiki/User:lindenb . Original java code from Martin Wattenberg, w(at)bewitched.com and Ben Bederson, bederson(at)cs.umd.edu University of Maryland, Human-Computer Interaction Lab http://www.cs.umd.edu/hcil"));


var rect= doc.createElementNS(Namespaces.SVG,"svg:rect");
svg.appendChild(rect);
rect.setAttribute("x","0");
rect.setAttribute("y","0");
rect.setAttribute("width",this.getWidth());
rect.setAttribute("height",this.getHeight());
rect.setAttribute("stroke","blue");
rect.setAttribute("fill","none");

if(this.treemapitems.length>0)
{
this.layout(this.treemapitems,new Rectangle(0,0,this.getWidth(),this.getHeight()));
}

for(i=0;i< this.treemapitems.length;i++ )
{
svg.appendChild(this.treemapitems[i].toSVG(this,doc));
}

return svg;
}

/* private */
TreeMap.prototype.layout=function(items, rect)
{
items.sort(treemap_item_compare);
this.layout2(items,0,items.length,rect);
}

/* private */
TreeMap.prototype.getWeight=function(items, start,end)
{
var sum=0.0;
while(start<end)
{
sum +=items[start].getWeight();
start++;
}
return sum;
}

/* private */
TreeMap.prototype.sliceLayout=function(comps,start,end,bounds)
{
var end=(comps.length<end?comps.length:end);
var total = this.getWeight(comps,start,end);
var a=0.0;
var vertical=(bounds.getWidth()<bounds.getHeight() );
var pos= (vertical==true?bounds.getY():bounds.getX());
var i;
for (i=start;i<end; i++)
{
var r=new Rectangle(0,0,0,0);
var b= comps[i].getWeight()/total;
if (vertical==true)
{
r.setX(bounds.getX());
r.setWidth(bounds.getWidth());
r.setY(pos);
var len = bounds.getHeight()*b;
r.setHeight(len);
pos+=len;
}
else
{
r.setX(pos);
var len = bounds.getWidth()*b;
r.setWidth(len);
r.setY(bounds.getY());
r.setHeight(bounds.getHeight());
pos+=len;
}
comps[i].setRectangle(r);
a+=b;
}
}

/* private */
TreeMap.prototype.layout2=function(comps,start,end,bounds)
{
if (start>=end) return;

if (end-start<2)
{
this.sliceLayout(comps,start,end,bounds);
return;
}

var x=bounds.getX();
var y=bounds.getY();
var w=bounds.getWidth();
var h=bounds.getHeight();

var total=this.getWeight(comps,start, end);
var mid=start;
var a= comps[start].getWeight()/total;
var b=a;

if (w<h)
{
// height/width
while (mid<end)
{
var aspect=this.normAspect(h,w,a,b);
var q= comps[mid].getWeight()/total;
if (this.normAspect(h,w,a,b+q)>aspect) break;
mid++;
b+=q;
}
this.sliceLayout(comps,start,mid+1,new Rectangle(x,y,w,(h*b)));
this.layout2(comps,mid+1,end,new Rectangle(x,(y+h*b),w,(h*(1-b))));
}
else
{
// width/height
while (mid<end)
{
var aspect=this.normAspect(w,h,a,b);
var q= comps[mid].getWeight()/total;
if (this.normAspect(w,h,a,b+q)>aspect) break;
mid++;
b+=q;
}
this.sliceLayout(comps,start,mid+1,new Rectangle(x,y,(w*b),h));
this.layout2(comps,mid+1,end,new Rectangle((x+w*b),y,(w*(1-b)),h));
}

}
/* private */
TreeMap.prototype.aspect=function(big,small,a,b)
{
return (big*b)/(small*a/b);
}
/* private */
TreeMap.prototype.normAspect=function(big, small,a,b)
{
x=this.aspect(big,small,a,b);
if (x<1) return 1.0/x;
return x;
}


}




function gm_xpath(expression,contextNode)
{
return document.evaluate(expression,contextNode,null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,null);
}

function getParameter(url,parameter)
{
if(url==null) return null;
var a= url.indexOf("?");
if(a==-1) return null;
if(url.indexOf(parameter+"=")==-1) return null;
var params= url.substring(a+1).split("&");
var i=0;
for(i=0;i<params.length;i++)
{
b= params[i].indexOf("=");
if(b==-1) continue;
var key = params[i].substring(0,b);
if(key!=parameter) continue;
return params[i].substring(b+1);
}
return null;
}

function escapeURL(url)
{
var s="";
var i=0;

for(i=0;i< url.length;++i)
{
var c=url.charAt(i)
switch( c )
{
case ':': s+= '%3A'; break;
case '/': s+= '%2F'; break;
case '?': s+= '%3F'; break;
case '=': s+= '%3D'; break;
case '&': s+= '%26'; break;
default : s+= c; break;
}
}
return s;
}

function Agent(name)
{
this.name=name;
this.count=1;
}


function insertToggle()
{

if(document.getElementsByTagName)
{
//hack found at http://erik.eae.net/archives/2005/06/10/22.21.42/#comment-5337
var inputElements = document.getElementsByTagName("input");
var i=0;
for (i=0; inputElements[i]!=null; i++)
{
inputElements[i].setAttribute("autocomplete","off");
}
}

var wrapper = document.getElementById("outer-wrapper");
if(wrapper==null)
{
GM_log('cannot find connotea div id="outer-wrapper"');
return;
}

var div1 = document.createElement("div");
div1.setAttribute("id","treemap-div");
div1.setAttribute("align","center");
div1.setAttribute("style","border:5px; margin: 5px; border: 1px solid #f00; font-family: Verdana, Arial, Helvetica, sans-serif; font-size:12pt;");
wrapper.parentNode.insertBefore(div1,wrapper);


var newanchor = document.createElement("a");
newanchor.setAttribute("title","show/hide treemap");
newanchor.setAttribute("href","javascript:void%200");
newanchor.appendChild( document.createTextNode("Treemap on/off"));
div1.appendChild(newanchor);

var div2 = document.createElement("div");
div2.setAttribute("id","treemap");
div2.setAttribute("align","center");
div1.appendChild(div2);


newanchor.addEventListener('click', function(event)
{

var treemapdiv = document.getElementById("treemap");
if(treemapdiv==null)
{
GM_log('cannot find connotea div id="treemap"');
return;
}
if(treemapdiv.hasChildNodes())
{
while(treemapdiv.hasChildNodes())
{
treemapdiv.removeChild(treemapdiv.firstChild);
}
}
else
{
var slashtagslash="/tag/";
var prefix="http://www.connotea.org"+slashtagslash;
var locationhref = window.location.href;
var ignoretag=null;
var num=getParameter(locationhref,"num");


var i=0;
var j=0;

i= locationhref.indexOf(slashtagslash);
if(i!=-1)
{
ignoretag = locationhref.substring(i+slashtagslash.length);
i= ignoretag.indexOf("?");
if(i!=-1) ignoretag= ignoretag.substring(0,i);
i= ignoretag.indexOf("/");
if(i!=-1) ignoretag= ignoretag.substring(0,i);
}


var main = document.getElementById("main");
var tags= new Array();

var wheight=window.outerHeight;
if(window.outerWidth< wheight) wheight=window.outerWidth;
wheight=wheight*0.6;
if(wheight<50) wheight=50;
var tm= new TreeMap(wheight,wheight);


if(main==null)
{
GM_log('cannot find connotea div id="main"');
return;
}
var divs= gm_xpath(".//div[@id]",main);
for(j=0; j<divs.snapshotLength; j++)
{
var d = divs.snapshotItem(j);
if(d.parentNode==null) continue;
var divid=d.id;
if(divid.indexOf("user_bookmark_")!=0) continue;

var allAnchors = gm_xpath(".//a[@href]",d);
for(i=0; i<allAnchors.snapshotLength; i++)
{
a = allAnchors.snapshotItem(i);
if(a.parentNode==null) continue;
var href=a.href;
if(href.indexOf(prefix)!=0) continue;
var tag=href.substring(prefix.length);
if( tag.length==0 ||
tag=="uploaded" ||
tag.indexOf("geo:")==0 ||
tag==ignoretag
) continue;


var k=0;
for(k=0;k< tags.length;++k)
{
if(tags[k].name==tag)
{
tags[k].count++;
break;
}
}
if(k==tags.length)
{
var newtag= new Agent(tag);
newtag.count=1;
tags.push(newtag);
}
}
}
for(i=0;i< tags.length;++i)
{
/** create a new item with weight=14 */
var item= new TreeMapItem(tags[i].count);
/* set stroke color */
item.setStroke(new Color(255,0,0));
/* set background color */
var gray= 220+ Math.floor(Math.random()*30.0);
item.setFill(new Color(gray,gray,gray));
/* set Label */
item.setLabel(tags[i].name);
//GM_log(tags[i].name+" "+tags[i].count);
/* set URL anchor */
item.setURL(prefix+tags[i].name+"?tool=gmtreemap"+(num==null?"":"&num="+num));
/* set title */
item.setTitle(tags[i].name+" ("+tags[i].count+")");
/* add the new item into the treemap */
tm.addItem(item);
}
//GM_log(tags.length);
var svg= tm.toSVG(document);
//GM_log(svg);
treemapdiv.appendChild(svg);
//GM_log("done");
}
},true);
}
window.addEventListener("load", insertToggle, false);






NCBI Pubmed, RSS feeds & Geotagging

Pubmed Queries can be saved as a RSS feed and today I used this tool to test the rss-to-georss-converter using the following terms: H5N1 AND epidemiology (this might interest Declan Butler...). The Geonames "RSS to GeoRSS Converter" reads the entries of an RSS feed and searches the Geonames Database to find a location for the entry text. If a relevant location is found, its latitude and longitude are added to the RSS feed using the GeoRSS encoding..




(...)
<item>
<title>Outbreak news. Avian influenza, Cambodia.</title>
<link>http://www.ncbi.nlm.nih.gov/entrez/query.fcgi?db=PubMed&amp;list_uids=166
73510
</link>
<description>
(...)Outbreak news. Avian influenza, Cambodia.(...)
Wkly Epidemiol Rec. 2006 Mar 31;81(13):117
</description>
<category>Wkly Epidemiol Rec</category>
<guid isPermaLink="false">PubMed:16673510</guid>
<dc:creator />
<geo:lat>11.55</geo:lat>
<geo:long>104.9166667</geo:long>
</item>
<item>
(...)


ok, what's next ?

Using the following XSLT sheet, the new feed can be transformed into a RIS file and imported and shared into connotea, exported to KML for google earth.

See the result in connotea at : http://www.connotea.org//user/testclient/tag/geotagged/date/2006-05-12


<?xml version='1.0' ?>
<xsl:stylesheet
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:gml="http://www.opengis.net/gml/"
xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
xmlns:georss="http://www.georss.org/georss/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
version="1.0"
>
<xsl:output method='text'/>



<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>

<xsl:template match="rss">
<xsl:apply-templates select="channel"/>
</xsl:template>

<xsl:template match="channel">
<xsl:apply-templates select="item"/>
</xsl:template>

<xsl:template match="item">
<xsl:if test="geo:lat and geo:long">

TY - JOUR
TI - <xsl:value-of select="title"/>
<xsl:call-template name="loop">
<xsl:with-param name="s"><xsl:value-of select="normalize-space(/rss/channel/title)"/></xsl:with-param>
</xsl:call-template>
KW - geotagged
KW - geo:lat=<xsl:value-of select="geo:lat"/>
KW - geo:long=<xsl:value-of select="geo:long"/>
UR - <xsl:value-of select="link"/>
ER -
<xsl:text>
</xsl:text>
</xsl:if>
</xsl:template>

<xsl:template name="loop">
<xsl:param name="s"/>
<xsl:choose>
<xsl:when test="contains(normalize-space($s),' ')">
<xsl:variable name="first" select="normalize-space(substring-before($s,' '))" />
<xsl:variable name="remain" select="normalize-space(substring-after($s,' '))" />
<xsl:if test="string-length($first)&gt;0 and $first!=&apos;PubMed:&apos;">
KW - <xsl:value-of select="$first"/>
</xsl:if>
<xsl:if test="string-length($remain)&gt;0">
<xsl:call-template name="loop">
<xsl:with-param name="s"><xsl:value-of select="$remain"/></xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:if test="string-length(normalize-space($s))&gt;0 and $s!=&apos;PubMed:&apos;">
KW - <xsl:value-of select="normalize-space($s)"/>
</xsl:if>
</xsl:otherwise>
</xsl:choose>

</xsl:template>

</xsl:stylesheet>


11 May 2006

Data Visualization: gapminder

If you're looking for a new way to visualize interactively your microarray data, have a glance at the charts from Gapminder (hosted at tools.google.com)



New BMC Journal: "Source Code for Biology and Medicine"

As discovered on chem-bla-ics:

Source Code for Biology and Medicine is an open access, peer-reviewed, online journal soon to be launched by BioMed Central. Source Code for Biology and Medicine will encompass all aspects of workflow for information systems, decision support systems, client user networks, database management, and data mining. Source Code for Biology and Medicine aims to publish source code for distribution and use in the public domain in order to advance biological and medical research. Through this dissemination, it may be possible to shorten the time required for solving certain computational problems for which there is limited source code availability or resources.

Unlike sourceforge, submiting a paper in this journal will allow scientists to get a new peer reviewed publication( may be critical for a carreer..). Moreover, I hope this journal will allow programmers (not pure theorists) to get a paper.


10 May 2006

Playing with the connotea API (2/2)

A few monthes ago Ben Lund gave me the opportunity to test a beta-version of the connotea API and I wondered if I was able to build an Annozilla server that could act as a bridge between the firefox web browser and the connotea server allowing scientists to see and share comments about a web site/ a paper. As it is said on the annozilla server:

The Annozilla project is designed to view and create annotations associated with a web page, as defined by the W3C Annotea project. The idea is to store annotations as RDF on a server, using XPointer (or at least XPointer-like constructs) to identify the region of the document being annotated. The intention of Annozilla is to use Mozilla's native facilities to manipulate annotation data - its built-in RDF handling to parse the annotations, and nsIXmlHttpRequest to submit data when creating annotations..

annozilla02
In this example: firefox opened the NCBI home page (right side). Once the page is loaded, annozilla fetches the bookmarks about NCBI from connotea (top left). Double clicking in the annotations makes annozilla download the body of those bookmarks (bottom left).


The JAVA servlet I wrote is available at :



But wait, there is a problem: For "security reasons" Annozilla does not use the "GET" parameters in an URL (I really understand that). So when the following URL is submited:

http://www.ncbi.nlm.nih.gov/entrez/query.fcgi?cmd=Retrieve&db=pubmed&dopt=Abstract&list_uids=14870871

Annozilla ignores all the parameters and inserts the comments for:

http://www.ncbi.nlm.nih.gov/entrez/query.fcgi



which is really less interesting !!!... Anyway, this was still a nice to write and it was proof of concept on how to use the connotea API.

update: 2010-08-12: source code

/*
Copyright (c) 2006 Pierre Lindenbaum PhD

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
``Software''), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

The name of the authors when specified in the source files shall be
kept unmodified.

THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL 4XT.ORG BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.


$Id: $
$Author: $
$Revision: $
$Date: $
$Locker: $
$RCSfile: $
$Source: $
$State: $
$Name: $
$Log: $


*************************************************************************/
package org.lindenb.annotea.server;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;


import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import com.oreilly.servlet.Base64Decoder;


/**
* @author lindenb
* http://islande:8080/annotea/Annotea
*/
public class AnnoteaServer extends HttpServlet
{
/**
* serialVersionUID
*/
private static final long serialVersionUID = 1L;
/** flag for debugging on/off */
private static boolean DEBUG=false;

//static declaration of xml namespaces
static public final String RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
static public final String RDFS = "http://www.w3.org/2000/01/rdf-schema#";
static public final String DC = "http://purl.org/dc/elements/1.1/";
static public final String XMLNS= "http://www.w3.org/2000/xmlns/";
static public final String AN = "http://www.w3.org/2000/10/annotation-ns#";
static public final String XHTML = "http://www.w3.org/1999/xhtml" ;
static public final String HTTP = "http://www.w3.org/1999/xx/http#";
static public final String CONNOTEA ="http://www.connotea.org/2005/01/schema#";
static public final String TERMS = "http://purl.org/dc/terms/";



/** query parameter name as specified in the spec */
static final String QUERY_ANNOTATION_PARAMETER="w3c_annotates";
/** default number of rows to fetch from http://www.connotea.org */
static final int CONNOTEA_DEFAULT_NUM_ROWS=10;
/** number of rows to fetch from http://www.connotea.org */
int connoteaNumRowsToFetch =CONNOTEA_DEFAULT_NUM_ROWS;



/** @see javax.servlet.GenericServlet#init() */
public void init() throws ServletException
{
//init number of rows
String s = getInitParameter("connoteaNumRowsToFetch");
if(s!=null)
{
try
{
this.connoteaNumRowsToFetch=Integer.parseInt(s);
if(this.connoteaNumRowsToFetch<=0) this.connoteaNumRowsToFetch=CONNOTEA_DEFAULT_NUM_ROWS;
}
catch(Exception err)
{
throw new ServletException(err);
}
}
}


/** convert a string to MD5 */
static String toMD5(String url) throws ServletException
{
StringBuffer result= new StringBuffer();
try
{
MessageDigest md = MessageDigest.getInstance("MD5");

md.update(url.getBytes());
byte[] digest = md.digest();

for (int k=0; k<digest.length; k++)
{
String hex = Integer.toHexString(digest[k]);
if (hex.length() == 1) hex = "0" + hex;
result.append(hex.substring(hex.length()-2));
}
}
catch(Exception err)
{
throw new ServletException(err);
}
return result.toString();
}

/** @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
{
res.setContentType("text/xml");
String urlquery= req.getParameter(QUERY_ANNOTATION_PARAMETER);
PrintWriter out=res.getWriter();

String pathInfo = req.getPathInfo(); // /a/b;c=123

/**
*
* WE FOUND URLQUERY
*
*/
if(urlquery!=null)
{
debug("query is "+urlquery);
String authorizationUTF8= URLEncoder.encode(getAuthorization(req),"UTF-8");
String md5=toMD5(urlquery);
Document doc= getConnoteaRDF(
"http://www.connotea.org/data/bookmarks/uri/"+md5,
req
);

String postCount=null;
String created=null;
if(doc!=null)
{
Element root= doc.getDocumentElement();
if(root==null || !isA(root,RDF,"RDF")) throw new ServletException("Bad XML root from connotea");

for(Node n1= root.getFirstChild();n1!=null;n1=n1.getNextSibling())
{
if(!isA(n1,TERMS,"URI")) continue;
for(Node n2= n1.getFirstChild();n2!=null;n2=n2.getNextSibling())
{
if(isA(n2,CONNOTEA,"postCount"))
{
postCount=textContent(n2).replace('\n',' ').trim();
}
else if(isA(n2,CONNOTEA,"created"))
{
created=textContent(n2).replace('\n',' ').trim();
}
}
break;
}
}

debug("postcount="+postCount);

out.print(
"<?xml version=\"1.0\" ?>\n" +
"<r:RDF xmlns:r=\""+RDF+"\"\n" +
" xmlns:a=\""+AN+"\"\n" +
" xmlns:d=\""+DC+"\">\n"
);
if(postCount!=null)
{
out.print(
" <r:Description r:about=\""+getBaseURL(req)+"/"+md5+"\">\n" +
" <r:type r:resource=\""+ AN +"Annotation\"/>\n" +
" <r:type r:resource=\"http://www.w3.org/2000/10/annotationType#Comment\"/>\n" +
" <a:annotates r:resource=\""+urlquery+"\"/>\n" +
" <d:title>"+postCount+" Annotation(s) of "+urlquery+" on connotea</d:title>\n" +
" <a:context>" + urlquery+"#xpointer(/html[1])</a:context>\n" +
" <d:creator>Connotea</d:creator>\n" +
" <a:created>"+created+"</a:created>\n" +
" <d:date>"+created+"</d:date>\n" +
" <a:body r:resource=\""+getBaseURL(req)+"/body/"+md5+"?authorization="+authorizationUTF8+"\">"+
"</r:Description>"
);
}

out.print("</r:RDF>\n");
}
/**
*
* /BODY/ in pathinfo
*
*/
else if(pathInfo!=null && pathInfo.startsWith("/body/"))
{
String md5=pathInfo.substring(6);
Document doc=getConnoteaRDF("http://www.connotea.org/data/uri/"+md5+
"?num="+this.connoteaNumRowsToFetch,
req);
if(doc==null)
{
throw new ServletException("Cannot get http://www.connotea.org/data/uri/"+md5);
}
Element root= doc.getDocumentElement();
if(root==null || !isA(root,RDF,"RDF")) throw new ServletException("Bad XML root from connotea");


out.print("<?xml version=\"1.0\" ?>\n"+
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
);

printHTMLBodyFromConnotea(new PrintWriter(new FileWriter("/tmp/logAnnotea.txt")),md5,root);
printHTMLBodyFromConnotea(out,md5,root);
}
/**
*
* other
*
*/
else
{
debug("pathinfo not handled ?");
out.print(
"<?xml version=\"1.0\" ?>\n" +
"<r:RDF xmlns:r=\""+RDF+"\"\n" +
" xmlns:a=\""+AN+"\"\n" +
" xmlns:d=\""+DC+"\">\n" +
"<a:annotates r:resource=\""+
req.getRequestURL()+ "\"/>"+
"</r:RDF>\n"
);
}


out.flush();
}

/** return and parse an HTML annotation from connotea */
private void printHTMLBodyFromConnotea(PrintWriter out,String md5,Element root)
{

out.print("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">"+
"<body>");
out.print("<img src=\"http://www.connotea.org/connotealogo.gif\" alt=\"connotea\"/><br/>");

for(Node n1= root.getFirstChild();n1!=null;n1=n1.getNextSibling())
{
if(isA(n1,CONNOTEA,"Post"))
{

String title=null;
String user=null;
String uri=null;
String created=null;
HashSet subject= new HashSet();
for(Node n2= n1.getFirstChild();n2!=null;n2=n2.getNextSibling())
{
if(isA(n2,DC,"creator")) { user=textContent(n2).trim();}
else if(isA(n2,CONNOTEA,"title")) { title=textContent(n2).trim();}
else if(isA(n2,CONNOTEA,"created")) {
created=textContent(n2).trim();
int T=created.indexOf('T');
if(T!=-1) created= created.substring(0,T);
}
else if(isA(n2,DC,"subject")) { subject.add(textContent(n2).trim());}
else if(isA(n2,CONNOTEA,"uri"))
{
for(Node n3= n2.getFirstChild();n3!=null;n3=n3.getNextSibling())
{
if(isA(n3,TERMS,"URI"))
{
Element e3=(Element)n3;
uri=e3.getAttributeNS(RDF,"about").trim();
}
}
}
}
out.print("<div>");

if(title!=null)
{
if(uri!=null) out.print("<h4><a target=\"ext\" title=\""+escape(uri)+"\" href=\""+escape(uri)+"\">");
out.print(""+escape(title));
if(uri!=null) out.print("</a>");
out.print(" (<a target=\"ext\" title=\"http://www.connotea.org/uri/"+md5+"\" href=\"http://www.connotea.org/uri/"+md5+"\">info</a>)");
out.print("</h4>");
}


if(user!=null)
{
out.print("Posted by <a target=\"ext\" href=\"http://www.connotea.org/user/"+user+"\">"+user+"</a>");
if(!subject.isEmpty())
{
out.print(" to");
for (Iterator iter = subject.iterator(); iter.hasNext();)
{
String sbj=escape((String)iter.next());
out.print(" <a target=\"ext\" href=\"http://www.connotea.org/tag/"+sbj+"\">"+sbj+"</a>");

}
}
if(created!=null ) out.print(" on <a target=\"ext\" href=\"http://www.connotea.org/date/"+created+"\">"+created+"</a>");
out.print("<br/>");
}
out.print("</div><hr/>");
}


}
out.print("<a title=\"mailto:plindenbaum@yahoo.fr\" href=\"mailto:plindenbaum@yahoo.fr\">plindenbaum@yahoo.fr</a> Pierre Lindenbaum PhD<br/>");
out.print("<div align=\"center\"><img border=\"1\" src=\"http://www.integragen.com/img/title.png\"/></div>");
out.print("</body></html>");
out.flush();
}


/**
* @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
{
Document doc=null;
try
{
doc= newDocumentBuilder().parse(req.getInputStream());
}
catch(SAXException err)
{
debug("cannot get document from input stream");
throw new ServletException(err);
}

String context=null;
String content=null;
Element root= doc.getDocumentElement();
if(root==null) throw new ServletException("Cannot find root element of input");
debug("DOM1 content is "+DOM2String(root));
for(Node c1= root.getFirstChild();c1!=null;c1=c1.getNextSibling())
{
if(!isA(c1,RDF,"Description")) continue;
for(Node c2= c1.getFirstChild();c2!=null;c2=c2.getNextSibling())
{
if(c2.hasChildNodes()&& isA(c2,AN,"context"))
{
Element e2=(Element)c2;
context=textContent(e2).trim();
int xpointer=context.indexOf("#xpointer");
if(xpointer!=-1) context=context.substring(0,xpointer);
}
else if(c2.hasChildNodes()&& isA(c2,AN,"body"))
{
for(Node c3= c2.getFirstChild();c3!=null;c3=c3.getNextSibling())
{
if(!(c3.hasChildNodes()&& isA(c3,HTTP,"Message"))) continue;

for(Node c4= c3.getFirstChild();c4!=null;c4=c4.getNextSibling())
{
if(!(c4.hasChildNodes()&& isA(c4,HTTP,"Body"))) continue;
content= textContent(c4).trim().replaceAll("[ ]+"," ");
debug("DOM content is "+DOM2String(c4));
}

}
}
}
}
if(context==null)
{
throw new ServletException("Cannot find context in "+root);
}
if(content==null)
{
throw new ServletException("Cannot find content in "+DOM2String(root));
}


/** check if boomarks already exists */
try
{
String usertitle=null;
String comment=null;
StringBuffer description=new StringBuffer();
HashSet tags= new HashSet();

doc= getConnoteaRDF(
"http://www.connotea.org/data/user/"+
getLogin(req)+"/uri/"+
toMD5(context),req
);
if(doc!=null)
{
root= doc.getDocumentElement();
if(root!=null)
{
for(Node n1= root.getFirstChild();n1!=null;n1=n1.getNextSibling())
{
if(isA(n1,CONNOTEA,"Post"))
{
for(Node n2= n1.getFirstChild();n2!=null;n2=n2.getNextSibling())
{
if(isA(n2,CONNOTEA,"title")) { usertitle=textContent(n2).trim();}
else if(isA(n2,DC,"subject")) { tags.add(textContent(n2).trim().toLowerCase());}
else if(isA(n2,CONNOTEA,"description")) { description= new StringBuffer(textContent(n2).trim()+"\n");}
}
break;
}
}
}
debug("existed: title="+usertitle+" subject="+tags+ " desc="+description);
}


/* construct URL to connotea */

BufferedReader buffReader= new BufferedReader(new StringReader(content));
debug("content is "+content);
String line=null;

while((line=buffReader.readLine())!=null)
{
line= line.trim();
String upper= line.toUpperCase();
//parse tags
if(upper.startsWith("TAG:"))
{
try
{
StreamTokenizer parser= new StreamTokenizer(
new StringReader(line.substring(4))
);
parser.quoteChar('"');
parser.quoteChar('\'');
parser.eolIsSignificant(false);
parser.lowerCaseMode(true);
parser.ordinaryChars('0','9');
parser.wordChars('0','9');
parser.whitespaceChars(',',',');
parser.whitespaceChars(';',';');
parser.whitespaceChars('(','(');
parser.whitespaceChars(')',')');

while ( parser.nextToken() != StreamTokenizer.TT_EOF )
{
if ( parser.ttype == StreamTokenizer.TT_WORD)
{
tags.add(parser.sval.toLowerCase());
}
else if ( parser.ttype == StreamTokenizer.TT_NUMBER )
{
//hum... ignoring number
//items.add(""+parser.nval);
}
else if ( parser.ttype == StreamTokenizer.TT_EOL )
{
continue;
}
else if(parser.sval!=null)
{
tags.add(parser.sval.toLowerCase()) ;
}
}
}
catch(Exception ex)
{

}
}
else if(upper.startsWith("TI:"))
{
usertitle= line.substring(3).trim();
if(usertitle.length()==0) usertitle=null;
}
else if(upper.startsWith("COM:"))
{
comment= line.substring(4).trim();
if(comment.length()==0) comment=null;
}
else
{
description.append(line+"\n");
}
}
//put one tag if no tags was declared
if(tags.isEmpty()) tags.add("annoteated");

if(description.length()==0) description= new StringBuffer(context);
//build tags parameter
StringBuffer tagStr= new StringBuffer();
for (Iterator iter = tags.iterator(); iter.hasNext();)
{
if(tagStr.length()!=0) tagStr.append(",");
tagStr.append(iter.next().toString());
}

debug("tagstr="+tagStr);


URL url=new URL("http://www.connotea.org:80/data/add");

String postbody=
"uri=" + URLEncoder.encode(context,"UTF-8")+
(usertitle==null?"":"&usertitle="+URLEncoder.encode(usertitle,"UTF-8"))+
"&description="+URLEncoder.encode(description.toString(),"UTF-8")+
(comment==null?"":"&comment="+URLEncoder.encode(comment,"UTF-8"))+
"&tags=" + URLEncoder.encode(tagStr.toString(),"UTF-8")+
"&annoteaflag=hiBenThisIsPierre"
;

StringBuffer poststring= new StringBuffer();
poststring.append("POST "+url.getFile()+" HTTP/1.1\n");
poststring.append("Host: "+url.getHost()+"\n");
poststring.append("authorization: "+getAuthorization(req)+"\n");
poststring.append("Content-length: "+postbody.length()+"\n");
poststring.append("\n");
poststring.append(postbody.toString());


Socket socket= new Socket(url.getHost(),url.getPort());
InputStream from_server= socket.getInputStream();
PrintWriter to_server= new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()));


to_server.print(poststring.toString());
to_server.flush();


StringBuffer response= new StringBuffer();
int c; while((c=from_server.read())!=-1) { response.append((char)c);}


to_server.close();
from_server.close();
debug("sent "+ poststring+" response is:\n"+response+"\n");
}
catch(Exception err)
{
debug("error "+err);
throw new ServletException(err);
}





{
String msg="<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
"<rdf:RDF xmlns:rdf=\""+RDF+"\"\n" +
" xmlns:a=\""+AN+"\"\n" +
" xmlns:dc=\""+DC+"\">\n" +
" <rdf:Description rdf:about=\""+
getBaseURL(req)+"/"+toMD5(context)
+"\">\n" +
" <dc:creator>connotea user</dc:creator>\n"+
" <a:created>2006-01-31</a:created>\n"+
" <dc:date>2006-01-31</dc:date>\n"+
" <a:annotates rdf:resource=\""+escape(context)+"\"/>\n" +
" <rdf:type rdf:resource=\""+ AN +"Annotation\"/>\n" +
" <rdf:type rdf:resource=\"http://www.w3.org/2000/10/annotationType#Comment\"/>\n" +
//" <a:body rdf:resource=\""+getBaseURL(req)+"/body/"+toMD5(context) +"\"/>\n" +
" </rdf:Description>\n" +
"</rdf:RDF>";



res.setStatus(HttpServletResponse.SC_CREATED);
res.setHeader("Connection","close");
res.setHeader("Pragma","no-cache");
res.setContentType("text/xml");
res.setContentLength(msg.length());



PrintWriter out= res.getWriter();
out.print(msg);
debug("message sent is "+msg);
out.flush();
}
}




/** return the BASE URL of this servet */
private static String getBaseURL(HttpServletRequest req) throws ServletException
{
String scheme = req.getScheme(); // http
String serverName = req.getServerName(); // hostname.com
int serverPort = req.getServerPort(); // 80
String contextPath = req.getContextPath(); // /mywebapp
String servletPath = req.getServletPath(); // /servlet/MyServlet
//String pathInfo = req.getPathInfo(); // /a/b;c=123
//String queryString = req.getQueryString(); // d=789
return scheme+"://"+serverName+":"+serverPort+contextPath+servletPath;
}

/** creates a new namespace aware DocumentBuilder parsing DOM */
private static DocumentBuilder newDocumentBuilder() throws ServletException
{
DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
factory.setCoalescing(true);
factory.setNamespaceAware(true);
factory.setExpandEntityReferences(true);
factory.setIgnoringComments(true);
factory.setValidating(false);
try
{
return factory.newDocumentBuilder();
} catch (ParserConfigurationException error)
{
throw new ServletException(error);
}
}

/** simple escape XML function */
public static String escape(String s)
{
if(s==null) return s;
StringBuffer buff= new StringBuffer();
for(int i=0;i< s.length();++i)
{
switch(s.charAt(i))
{
case('\"') : buff.append("&quot;"); break;
case('\'') : buff.append("&apos;"); break;
case('&') : buff.append("&amp;"); break;
case('<') : buff.append("&lt;"); break;
case('>') : buff.append("&gt;"); break;
default: buff.append(s.charAt(i)); break;
}
}
return buff.toString();
}

/** simple test for XML element */
static public boolean isA(Node e,String ns, String localname)
{
if(e==null) return false;
return ns.equals(e.getNamespaceURI()) && e.getLocalName().equals(localname);
}

/** get text content of a DOM node */
static public String textContent(Node node)
{
return textContent(node,new StringBuffer()).toString();
}

/** get text content of a DOM node */
static private StringBuffer textContent(Node node,StringBuffer s)
{
if(node==null) return s;
for(Node c= node.getFirstChild();c!=null;c=c.getNextSibling())
{
if(isA(c,XHTML,"br"))
{
s.append("\n");
}
else if(c.getNodeType()==Node.CDATA_SECTION_NODE)
{
s.append(((CDATASection)c).getNodeValue());
}
else if(c.getNodeType()==Node.TEXT_NODE)
{
s.append(((Text)c).getNodeValue());
}
else
{
textContent(c,s);
}
}
return s;
}


/** download a document from connotea or null if the url was not found */
private Document getConnoteaRDF(String url,HttpServletRequest req) throws ServletException
{
DocumentBuilder builder= newDocumentBuilder();
try
{
return builder.parse(
openSecureURLInputStream(
url,
req
)
);
}
catch (FileNotFoundException e)
{
debug("file not found for "+url+" returning empty rdf document");
return null;
}
catch(Exception err)
{
debug("cannot download :"+url+ " "+err);
throw new ServletException(err);
}
}


/** get login from header authorization */
static private String getLogin(HttpServletRequest req) throws ServletException
{
return getLoginAndPassword(req)[0];
}

/** get login and passord from header authorization */
static private String[] getLoginAndPassword(HttpServletRequest req) throws ServletException
{
String s64= getAuthorization(req);
if(!s64.startsWith("Basic ")) throw new ServletException("header \"authorization\" does not start with \"Basic \" ");
s64=s64.substring(6);
String decoded= Base64Decoder.decode(s64);
int loc= decoded.indexOf(':');
if(loc==-1) throw new ServletException("no \":\" in decoded authorization "+s64);
return new String[]{decoded.substring(0,loc),decoded.substring(loc+1)};
}

/** find parameter authorization in http request */
static String getAuthorization(HttpServletRequest req) throws ServletException
{
String s64= req.getHeader("authorization");
if(s64==null)
{
s64=req.getParameter("authorization");
}
if(s64==null)
{
s64=req.getParameter("Authorization");
}
if(s64==null)
{
Enumeration e= req.getHeaderNames();
StringBuffer headers= new StringBuffer();
while(e.hasMoreElements())
{
String key=(String)e.nextElement();
headers.append(key).append("=").append(req.getHeader(key)).append(";\n");
}
throw new ServletException("no header \"authorization\" was found in\n"+headers);
}
return s64;
}

/** open a URL, filling authorization header */
static private InputStream openSecureURLInputStream(String urlstr,HttpServletRequest req)
throws ServletException,FileNotFoundException
{
URL url=null;
String s64=null;
try
{
s64= getAuthorization(req);
url= new URL(urlstr);
URLConnection uc = url.openConnection();
uc.setRequestProperty("Authorization", s64);
InputStream content = uc.getInputStream();
return content;
}
catch (MalformedURLException e)
{
throw new ServletException(e);
}
catch (FileNotFoundException e)
{
throw e;
}
catch (IOException e)
{
throw new ServletException(e);
}
}


/** quick n dirty debugging function: append the message to "/tmp/logAnnotea.txt" */
static private void debug(Object o)
{
if(!DEBUG) return;
try
{
File f= new File("/tmp/logAnnotea.txt");
PrintWriter pout= new PrintWriter(new FileWriter(f,true));
pout.println("###"+System.currentTimeMillis()+"########");
pout.println(o);
pout.flush();
pout.close();

}
catch (Exception err)
{
err.printStackTrace();
}

}

private String DOM2String(Node doc)throws ServletException
{
StringWriter out= new StringWriter();
printDOM(new PrintWriter(out,true),doc);
return out.toString();
}

/* print DOM document for debugging purpose...*/
private static void printDOM(PrintWriter log,Node doc) throws ServletException
{
Source source = new DOMSource(doc);
Result result = new StreamResult(log);


try
{
Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.transform(source, result);
}
catch(TransformerException error)
{
error.printStackTrace();
error.printStackTrace(log);
log.flush();
throw new ServletException(error);
}
}


}

Playing with the connotea API (1/2)

Connotea is a free online reference management service. It allows you to save links to all your favourite articles, references, websites and other online resources with one click. Connotea is also a social bookmarking tool, so you can view other people's collections to discover new, interesting content. Playing with Connotea is a pretext for me to learn new technologies and as the connotea web API has just been released I wanted to learn AJAX: so I transformed my previous 'old' tool connotea explorer into a dynamic javascript page available at:

http://www.urbigene.com/cx2/connotea.xhtml


An old screenshot of connotea explorer, the result with the html/javascript page is rather the same...


The page displays data from Connotea using SVG, javascript, AJAX, XSLT and the Connotea API using a treemap algorithm. As Firefox now supports the SVG format, this drawing can be displayed in your web browser.

Security Issue: As this script is not hosted on the connotea server, the user is asked to enable his web browser to connect to connotea (UniversalBrowserRead). The user has to open his browser at about:config and check that signed.applets.codebase_principal_support is set to true to get a dialog for enabling the connection to the connotea server) yes this is ugly... :-).