Topic Maps Query Service – YQL for Topic Maps?

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:

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)

Leave a comment

3 Comments

  1. Aki

     /  24/11/2010

    Excellent 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.

    Reply
  2. mimnimalismore

     /  24/11/2010

    Yesyesyess! 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?

    Reply
  3. Thanks 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…

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

  • @twitter

  • Tags

  • Topics

  • Recent Comments

  • Topic Map Feeds