As I was playing with YUI3 and YQL (an awesome abstraction above and across web services — select * from internet) last weekend, I noticed that there is an open data table for sparql search.
Not knowing whether such a thing already exists for topic maps, I created a simple Topic Maps search servlet that can be used to query any topic map available on the web – as long as both the query language and the topic map format used are supported by Ontopia.
It’s just a prototype created for fun, but here are some examples of what could be done with such a service:
- All topics with a name like “tosca” in the Opera topic map hosted on www.ontopia.net:
select $t from topic-name($t, $n), value-like($n, "tosca")? - All blog posts in this blog’s XTM feed:
select $blogpost from instance-of($blogpost, $type), subject-identifier($type, $si), $si = "http://psi.ontopedia.net/Blogging/Post"? - Topics whose occurrence(s) are like “fleece” in the Stormberg demo topic map hosted at google code:
select $item, $occ from occurrence($item, $occ), value-like($occ, "fleece")?
The results are presented as a JTM 1.1-ish documents.
No need to run a topic maps engine locally, and the results could easily be integrated into a web app using e.g. YUI.
It’s very buggy (e.g. might try to cast Float to TopicIF – sigh), but feel free to try it out at http://billy-corgan.com/yql?query={query}&topicmap={uri-to-topicmap}.
Here’s the Java servlet code:
package com.topicobserver.topicmaps.yql;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.infoset.impl.basic.URILocator;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.TopicMapReaderIF;
import net.ontopia.topicmaps.core.TopicNameIF;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.core.QueryProcessorIF;
import net.ontopia.topicmaps.query.core.QueryResultIF;
import net.ontopia.topicmaps.query.utils.QueryUtils;
import net.ontopia.topicmaps.utils.ImportExportUtils;
import net.ontopia.utils.OntopiaRuntimeException;
import org.json.simple.JSONObject;
public class TopicMapSearch extends HttpServlet {
private static final long serialVersionUID = 1L;
private HttpServletRequest request;
private HttpServletResponse response;
private TopicMapIF topicMap;
private String searchQuery;
private String topicMapUri;
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
this.request = req;
this.response = res;
this.searchQuery = request.getParameter("query");
this.topicMapUri = request.getParameter("topicmap");
if(searchQuery == null || "".equals(searchQuery) ||
topicMapUri == null || "".equals(topicMapUri)) {
response.sendError(500);
return;
}
this.processRequest();
}
private void processRequest() throws IOException {
response.setContentType("application/json");
try {
TopicMapReaderIF reader = ImportExportUtils.getReader(new URILocator(topicMapUri));
topicMap = reader.read();
} catch (IOException e1) {
return;
}
String json = "";
PrintWriter out = response.getWriter();
// Execute search
try {
json = this.searchTopicMap();
} catch (InvalidQueryException e) {
response.sendError(500);
} catch (OntopiaRuntimeException e) {
response.sendError(500);
}
out.print(json);
out.close();
}
private String searchTopicMap()
throws OntopiaRuntimeException, InvalidQueryException {
QueryProcessorIF proc = QueryUtils.getQueryProcessor(topicMap);
QueryResultIF result = proc.execute(searchQuery);
String[] variables = result.getColumnNames();
Object[] row = new Object[result.getWidth()];
TMObjectIF currentTmObject;
// OK, so I wrote this before I went looking for json.simple.JSONObject
// and I am too lazy to refactor just for fun.
int i = 0;
StringBuffer json = new StringBuffer("{ \"result\": [\n");
while (result.next()) {
result.getValues(row);
json.append((i++ > 0) ? ",\n" : "\n");
json.append("[");
for (int ix = 0; ix < variables.length; ix++) {
currentTmObject = (TMObjectIF) row[ix];
if (currentTmObject == null) {
continue;
}
json.append((ix > 0) ? ",\n" : "");
json.append("{ \"variable\": \"" + jsonEncode(variables[ix]) + "\",");
json.append(" \"tm_object\": {" + jsonify(currentTmObject) + "}}");
}
json.append("]");
}
json.append("]\n");
json.append(", \"query\": \"" + jsonEncode(searchQuery) + "\"");
json.append(", \"topicmap\": \"" + jsonEncode(topicMapUri) + "\"");
json.append("}");
result.close();
return json.toString();
}
private StringBuffer jsonify(TMObjectIF tmObject) {
StringBuffer json = new StringBuffer();
if (tmObject instanceof TopicIF) {
json.append(jsonify((TopicIF) tmObject));
} else if(tmObject instanceof OccurrenceIF) {
json.append(jsonify((OccurrenceIF) tmObject));
} else if(tmObject instanceof AssociationIF) {
json.append(jsonify((AssociationIF) tmObject));
} else {
// Could be TopicName etc...
// Could've cared had I been paid to do this.
json.append("\"construct\": \"unknown\"");
json.append(",\"string-value\": \"" + jsonEncode(tmObject.toString()) + "\"");
}
Collection<LocatorIF> iris = tmObject.getItemIdentifiers();
json.append(json.length() > 0 ? "," : "");
json.append("\"item_identifiers\" : [" + locatorsAsCsv(iris) + "]");
return json;
}
// in the lack of util methods to transform TopicIF to json object
private StringBuffer jsonify(TopicIF topic) {
StringBuffer json = new StringBuffer();
Collection<LocatorIF> sis = topic.getSubjectIdentifiers();
json.append("\"construct\": \"topic\"" +
", \"subject_identifiers\" : [" + locatorsAsCsv(sis) + "]" +
", \"names\": [" + namesAsJson(topic) + "]" );
return json;
}
private StringBuffer jsonify(OccurrenceIF occurrence) {
StringBuffer json = new StringBuffer();
json.append("\"construct\": \"occurrence\"" +
", \"value\": \"" + jsonEncode(occurrence.getValue()) + "\"");
return json;
}
private StringBuffer jsonify(AssociationIF association) {
StringBuffer json = new StringBuffer();
json.append("\"construct\": \"association\"");
return json;
}
private StringBuffer namesAsJson(TopicIF topic) {
StringBuffer json = new StringBuffer();
Collection<TopicNameIF> names = topic.getTopicNames();
if(names == null || names.size() == 0) {
return json;
}
for( TopicNameIF name : names) {
json.append("{ \"value\": \"" + jsonEncode(name.getValue()) + "\"");
json.append(", \"item_identifiers\": [" + locatorsAsCsv(name.getItemIdentifiers()) + "]");
Collection<TopicIF> scope = name.getScope();
json.append(", \"scope\": [");
for( TopicIF scopeTopic : scope ) {
json.append(locatorsAsCsv(scopeTopic.getItemIdentifiers()));
}
json.append("]");
json.append("},");
}
json.deleteCharAt(json.length()-1);
return json;
}
private StringBuffer locatorsAsCsv(Collection<LocatorIF> locators) {
StringBuffer csvStr = new StringBuffer();
if(locators == null || locators.size() == 0) {
return csvStr;
}
for(LocatorIF locator : locators) {
if(locator != null) {
csvStr.append("\"" + jsonEncode(locator.getExternalForm()) + "\",");
}
}
csvStr.deleteCharAt(csvStr.length()-1); // strip off last comma
return csvStr;
}
private String jsonEncode(String str) {
return JSONObject.escape(str);
}
}
(and no, I never did create a YQL table for this)
Aki
/ 24/11/2010Excellent work! It’s quite a coincidence I have been examining the YQL lately too, and have pondered, how could I implement a general transformation scheme from YQL results to topic maps. It never occurred to me, one could use YQL to query any topic map too. Well, I hope your experiment doesn’t stop here but results an YQL interface for Ontopia hosted topic maps.
mimnimalismore
/ 24/11/2010Yesyesyess! That would be absolutely awesome! A good way of spreading topic mapping to the people. Maybe one should sit down and write Ontopia code for such a feature?
Trond
/ 30/11/2010Thanks for the responses
@mimnimalismore Yeah, IMO this is something the Ontopia and/or TM communities could do in order to make it easier for apps to consume “mapped data”
For me it was more of a fun thing to do over the weekend, though, as I am currently not involved in TM work…