Quotation Chrome Extension

December 4, 2013 1 comment

I’m creating a Chrome extension that allows a user to search for a quotation. I’d like to have a toolbar popup which allows the user to enter a search query and receive a list of matching quotations.

toolbar-search-1280x800

Creating and installing an extension is straight forward and already well documented. I’ll focus on the more specific aspects of the extension. Version one will do the following:

  • Provide a browser action which installs a toolbar item in Chrome’s toolbar.
  • When the user clicks on the extension’s toolbar icon a popup with a simple input field and search button will appear.
  • Upon clicking on the search button the extension will query Freebase for matching quotations and display the results in the popup.

Every Chrome extension contains a manifest file (manifest.json) which defines it. Here’s part of my manifest …

"browser_action": {
 "default_popup": "popup.html"
 },</pre>
"content_security_policy": "script-src 'self' https://apis.google.com https://ajax.googleapis.com https://ssl.google-analytics.com; object-src 'self'"
<pre>

When the Quotation toolbar icon is clicked popup.html will be displayed as a popup below the icon. The content_security_policy setting is required to allow calls to Google’s client and analytic API’s.

Using JQuery I’ll create a callback when the search button is clicked.

From popup.html …

</pre>
<form>
<table style="border-spacing: 0px; width: 80%;">
<tbody>
<tr>
<td style="margin: 0px; padding: 0px;">
<input type="search" id="query-field" style="margin: 0px; padding: 5px 3px 3px 7px; width: 100%; font-size: 15px;" autofocus/>
</td>
<td style="margin: 0px; padding: 0px; background-color: RoyalBlue;" align="center">
<input type="image" src="search.png" alt="Search" />
</td>
</tr>
</tbody>
</table>
 </form>
<pre>

The search script will listen for a click on the search button and when it occurs will send a search request to the Freebase search API and update the result list. Here’s an abridged version of the code.


function search() {</pre>
query = $('#query-field').val();
request = gapi.client.request({
'path': '/freebase/v1/search',
'params': {
'query': query,
'filter': '(any type:/media_common/quotation)',
'cursor': cursor,
'limit': limit
}
 });
 request.execute(function(json) {
length = json.result.length;

if (length != 0) {
$("#quotation-list").html('');
for (var i = 0; i < length; i++) {
var quotation = json.result[i].name;
var mid = json.result[i].mid;
var url = "http://quotation.bwgz.org/quotation" + mid;

         var id = "item-" + i;

         $("#quotation-list").append('<div class="result"><a id="' + id + '" href="' + url + '" data-mid="' + mid + '" target="_blank">' + json.result[i].name + '</a></div>');
}
 }
<span style="font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 18px;">});</span>

successI use the Google Client API to call Freebase’s search API. It returns a set of search results in a JSON wrapper. The code loops through the results and updates the page accordingly.

I limit results to 10 items per page and additional code handles the paging.  The Freebase search API will only supply the first 220 highest scoring results.  While this is a bit of an annoying limitation few users would page through more results than that.

Clicking on a search results causes the browser to open a new tab and sends the user to quotation.bwgz.org where they’ll see more details on the quotation.
Quotation
 

Google Freebase Client Library – Topic API

The following comment was recently posted on the Freebase discussion mailing list …

Yes, we are actually deprecating it [Text] in favor of getting a description from the Topic API. The good part is that the new solution will return you entity descriptions across 40 languages from Wikipedia (on top of course of any user entered descriptions).
We haven’t announced it yet since we just finished that feature, but we will do so soon.

That prompted me to look again at Google’s Freebase client library. The Topic API returns all the known facts for a given topic including images and text blurbs. At first blush it appeared to be a bit of overkill for what I needed but a second looked showed that it was pretty straight forward to deal with.

It takes a bit time to become comfortable with Topic’s response. It’s a hierarchy of maps and lists represented by a JSON object. Here’s how I wrapped my brain around it.

At the root level of the response there is an object ({}) named “property”. It’s a map of domain properties. If you didn’t filter the request then you’re getting back all the domains associated with the topic. If you did apply a filter then you’ll only get back those domains you specified.

This is a response fragment containing a domain object.

"/type/object/name": {
"valuetype": "string",
"values": [
{
"text": "William Shakespeare",
"lang": "en",
"value": "William Shakespeare",
"creator": "/user/santiago_aguiar",
"timestamp": "2011-03-15T12:51:40.000Z"
}
],
"count": 39.0
}

Each domain is an object ({}) with the following properties:

Name Type Description
valuetype string The name of the “property value”
values array Any array ([]) of values where a value is a object ({}).
count integer The total number of values that exist in Freebase.”values” is an array of objects.

The values array contains objects with a consistent form.

If the value type is “float”, “int”, “bool”, “datetime”, “uri”, “key”, or “object” each object in the array will be of the following form.

Name Type Description
text string
lang string The language type
value many This can one of the following:

  • literal – a primitive such as a string, integer, or boolean
  • foreign key – the id (a string) of a topic contained in another dataset
  • topic reference – the id (a string) of another topic
creator string Id of the value’s creator.
timestamp string When the value was created.

There can be additional properties. For example, the domain /common/topic/description will add a “citation” object.

"/common/topic/description": {
"valuetype": "string",
"values": [
{
...
"citation": {
"provider": "Wikipédia",
"statement": "Description licensed under the Creative Commons Attribution-ShareAlike License (http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License).",
"uri": "http://fr.wikipedia.org/wiki/William_Shakespeare"
}
}
],
"count": 39.0
}

Things are straight forward when the value type is a literal, foreign key, or topic reference. In those cases “value” holds a holds something which can be dealt with simply.

When the value type is “compound” things get a bit more interesting.

Name Type Description
text string
lang string The language type
id string The id (a string) of the topic contained in “property”.
creator string Id of the value’s creator.
timestamp string When the value was created.
property string A map of the topic’s (see id) properties.

A “compound” object nests another topic as a property value. The “value” property is gone and replaced by “id” and “property”. “id” holds the id of the topic described in “property”. This parallels the response that Topic returns when queried.

With that understanding in place I tried out the Topic API found in the Google Freebase client library. Here’s some sample code.


HttpTransport httpTransport = new NetHttpTransport();
JsonFactory jsonFactory = new JacksonFactory();
HttpRequestInitializer httpRequestInitializer = new HttpRequestInitializer() {
  @Override
  public void initialize(HttpRequest request) throws IOException {}
};

Freebase.Builder fbb = new Freebase.Builder(httpTransport, jsonFactory, httpRequestInitializer);
fbb.setApplicationName("freebase-test");
Freebase freebase = fbb.build();

try {
 Freebase.Topic.Lookup lookup = freebase.topic().lookup(Arrays.asList(mid));
 TopicLookup topic = lookup.execute();
 if (topic != null) {
  System.out.printf("topic: %s\n", topic.getId());

  Property property = topic.getProperty();
  if (property != null) {
   System.out.println(property);
   }
  }
 }
} catch (IOException e) {
 e.printStackTrace();
}

So far so good. The client code executes correctly and I get back a valid response. But at this point I hit a wall. The Property class doesn’t do anything specific with the response. It’s a sub-class of GenericJson and that means all the properties are held in a very generic way. I was a bit surprised by this given that there are classes (TopicPropertyvalue and TopicValue) in place to hold topic properties and values. Since this library is a work in progress maybe the work hasn’t progressed that far. I didn’t want to go slogging through maps and arrays so I modified Property to use them. I changed its super class from GenricJson to ArrayMap<String, TopicPropertyvalue>. With that in place I could now output the first value of all the topic’s properites quite simply …

// for each domain in the property map
for (Object name : topic.getProperty().keySet()) {
 TopicPropertyvalue tpv = property.get(name); // get the domain name
 String valuetype = tpv.getValuetype();       // get the value type
 List<TopicValue> values = tpv.getValues();   // get the list of values
 // translate the value type to the key needed to get the "property value"
 // this will translate to value, id, or property
 String key = ValueType.valueOf(valuetype.toUpperCase()).getKey();
 Object value = values.get(0).get(key);       // from the first value object get the "primary value"

 System.out.printf("\t domain: %s primary value (%s): %s\n", name, valuetype, value);
}

Not too bad.

Categories: Bruce's Posts, Freebase

Google Freebase Client Library

March 30, 2013 Leave a comment

Google’s APIs Client Library for Java provides a way to utilize Google APIs using JSON and OAuth 2.0. One library provides access to Freebase and this post describes my experiences using it.

It quickly became clear that this library is a work in progress. It labels itself as beta but I’d describe it as alpha. The documentation is sparse, I couldn’t find good Java samples, and the library’s functionality is incomplete. Having said that, this library could be quite useful once completed.

Freebase’s HTTP API is well documented and you can find samples such as this that use Google’s HTTP client library but not the Freebase library.

public class FreebaseSearchExample {
 public static String API_KEY = "YOUR-API-KEY-GOES-HERE";
 public static void main(String[] args) {
  try {
    HttpTransport httpTransport = new NetHttpTransport();
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
    JSONParser parser = new JSONParser();
    GenericUrl url = new GenericUrl("https://www.googleapis.com/freebase/v1/search");
    url.put("query", "Cee Lo Green");
    url.put("filter", "(all type:/music/artist created:\"The Lady Killer\")");
    url.put("limit", "10");
    url.put("indent", "true");
    url.put("key", FreebaseSearchExample.API_KEY);
    HttpRequest request = requestFactory.buildGetRequest(url);
    HttpResponse httpResponse = request.execute();
    JSONObject response = (JSONObject)parser.parse(httpResponse.parseAsString());
    JSONArray results = (JSONArray)response.get("result");
    for (Object result : results) {
      System.out.println(JsonPath.read(result,"$.name").toString());
    }
  } catch (Exception ex) {
    ex.printStackTrace();
  }
 }
}

All calls to Freebase are returned within a JSON envelop. One of the biggest decisions a programmer must make is how to unmarshal the reply. The example above stores the response in vanilla JSONArray object. This is all well and good but further use of the results requires sifting through arrays for the desired objects. Later I’ll show how to parse the response into a POJO.

The response from Freebase contains two parts. One, a JSON envelope that either represents a successful execution of the request or an error message. The examples I’ve seen don’t deal with error checking or handling. At this point I won’t either and leave that for another post. A successful response from MQL Read will look like this.

{
"result": [
{
"name": "Jack Abramoff",
"type": "/people/person"
},
{
"name": "Bob Ney",
"type": "/people/person"
},
{
"name": "David Safavian",
"type": "/people/person"
}
],
}

In this case “result” contains the data requested. Other API calls, such as Topic, return different name/value pairs. The envelop can also contain values such as cost, cursor, and status. The unmarshalling code needs to handle (either accept and/or ignore) all the values returned in the envelop.

The Freebase client library supports the following APIs.

Freebase API Freebase Client Class Description Functional
Text Freebase.Text Returns a blob of HTML data pertaining to a specified id. Yes
Topic Freebase.Topic Returns properties and meta-data about a topic at a specified id. Yes
Image Freebase.Image imageReturns the scaled/cropped image attached to a freebase node. No
MQL Read Freebase.Mqlread Query the Freebase database using the Metaweb query language (MQL). No
MQL Write Freebase.Mqlwrite Write to the Freebase database No

For some reason the library does not support Search or Reconciliation.

The Text API returns a result containing a blob (blurb) about the specified topic. For some reason it’s not described in the Freebase developer guide. One answer on stackoverflow describes it as …

The freebase text api (http://wiki.freebase.com/wiki/ApiText) gives you back a lot of the top blurb from the wikipedia page.

I think this is all the that Freebase (and DBPedia) store about the topic. To get the full text of the wikipedia article, you can go directly to wikipedia.

For example a call to the Text API using Albert Einstein’s mid produces the following.


Request:

https://www.googleapis.com/freebase/v1/text/m/0jcx

Response:
{ “result”: “Albert Einstein ( /ˈælbərt ˈaɪnstaɪn/; German: [ˈalbɐt ˈaɪnʃtaɪn] ( listen); 14 March 1879 – 18 April 1955) was a German theoretical physicist who developed the theory of general relativity, effecting a revolution in physics. For this achievement, Einstein is often regarded as the father of modern physics. Einstein is generally considered the most influential physicist of the 20th century. While best known for his mass–energy equivalence formula E = mc (which has been dubbed \”the world’s most famous equation\”), he received the 1921 Nobel Prize in Physics \”for his services to theoretical physics, and especially for his discovery of the law of the photoelectric effect\”. The latter was pivotal in establishing quantum theory within physics.\nNear the beginning of his career, Einstein thought that Newtonian mechanics was no longer enough to reconcile the laws of classical mechanics with the laws of the electromagnetic field. This led to the development of his special theory of relativity. He realized, however, that the principle of relativity could also be extended to gravitational fields, and with his subsequent theory of gravitation in 1916, he published a paper on the general theory” }

Here’s a sample that uses the Text client API

public class GetTest {
 public static void main(String[] args) {
  HttpTransport httpTransport = new NetHttpTransport();
  JsonFactory jsonFactory = new JacksonFactory();
  HttpRequestInitializer httpRequestInitializer = new HttpRequestInitializer() {
   @Override
   public void initialize(HttpRequest request) throws IOException {
   }
  };

  Freebase.Builder fbb = new  Freebase.Builder(httpTransport, jsonFactory,
          httpRequestInitializer);
  Freebase freebase = fbb.build();
  ArrayList list = new ArrayList();
  try {
   list.add("/m/0jcx");
   Freebase.Text.Get text = freebase.text().get(list);
   ContentserviceGet get = text.execute();
   System.out.println(get.toPrettyString());
  } catch (Exception e) {
  e.printStackTrace();
  }
 }
}

ContentserverGet is a simple Class class to receive the JSON envelop.

public final class ContentserviceGet extends GenericJson {

 @com.google.api.client.util.Key
 private String result;

 public String getResult() {
  return result;
 }

 public ContentserviceGet setResult(String result) {
  this.result = result;
  return this;
 }
}

@com.google.api.client.util.Key specifies that a field is a data key, optionally providing the data key name to use. The JSON parser uses this key to unmarshal the JSON “result” value into the result field. In this case things are pretty simple because we’re only dealing with a string.

The Topic API returns all the known facts for a given topic including images and text blurbs. It will return a lot of data. It’s like getting back everything but the kitchen sink. Clink on this link for an example. The request also supports a filter property to reduce the amount of information returned. A call to the Topic API returns two root level values (id and property). Property contains a map of all the associated properties. Here’s a sample that uses the Topic client API.

public class TopicTest {
 public static void main(String[] args) {
  HttpTransport httpTransport = new NetHttpTransport();
  JsonFactory jsonFactory = new JacksonFactory();
  HttpRequestInitializer httpRequestInitializer = new HttpRequestInitializer() {

   @Override
   public void initialize(HttpRequest request) throws IOException {
   }
  };

  Freebase.Builder fbb = new  Freebase.Builder(httpTransport, jsonFactory,
      httpRequestInitializer);
  Freebase freebase = fbb.build();
  ArrayList list = new ArrayList();
  try {
   list.add("/m/0jcx");
   Freebase.Topic.Lookup lookup = freebase.topic().lookup(list);
   TopicLookup topic = lookup.execute();
   System.out.println(topic.toPrettyString());
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

In this case TopicLookup, which is a little more complex than ContentserverGet, receives the JSON envelop. If suffice to say that TopicLookup is filled with the map of all the properties returned and can be programmatically traversed as desired.

The MQL Read API provides access to the Freebase database using the Metaweb query language (MQL). For example, the following call will query for Albert Einstein’s children.


Request:
https://www.googleapis.com/freebase/v1/mqlread?query={"type":"/people/person","id":"/en/albert_einstein","children":[]}

Result:
{
“result”: {
“children”: [
"Eduard Einstein",
"Hans Albert Einstein",
"Lieserl Einstein",
"Ilse Einstein",
"Margot Einstein"
],
“type”: “/people/person”,
“id”: “/en/albert_einstein”
}
}

Freebase.Mqlread reminds me of all the empty cities in China. Everything needed is there but not used. It’s currently coded as following …

Mqlread extends FreebaseRequest<Void>

This results in the query being executed but nothing returned to the caller.

I wanted to try it out so I modified the source to use a generic type …

Mqlread<T> extends FreebaseRequest<T>

I also needed to pass along the generic’s Class when instantiating Mqlread.

public Mqlread mqlread(String query, Class<?> classType) throws java.io.IOException {
  Mqlread result = new Mqlread(query, classType);
  initialize(result);
  return result;
}

public class Mqlread extends FreebaseRequest {

...

  protected Mqlread(String query, Class classType) {
    super(Freebase.this, "GET", REST_PATH, null, classType);
    this.query = Preconditions.checkNotNull(query, "Required parameter query must be specified.");
  }

...

}

The following code uses the modified Freebase.Mqlread to fetch Albert Einstein’s children.

public class MQLReadTest {
	static private final String query =
			"{\"type\":\"/people/person\",\"id\":\"/en/albert_einstein\",\"children\":[]}";

	public static void main(String[] args) {
	    HttpTransport httpTransport = new NetHttpTransport();
	    JsonFactory jsonFactory = new JacksonFactory();
	    HttpRequestInitializer httpRequestInitializer = new HttpRequestInitializer() {

			@Override
			public void initialize(HttpRequest request) throws IOException {
			}
	    };

	    Freebase.Builder fbb = new  Freebase.Builder(httpTransport, jsonFactory, httpRequestInitializer);
	    Freebase freebase = fbb.build();
	    try {
	        Freebase.Mqlread mqlRead = freebase.mqlread(query, GenericJson.class);
	        GenericJson response = mqlRead.execute();
	        System.out.println(response.toPrettyString());
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}
}

Using GenericJson as the generic causes the parser to create a map of the response returned by the query. This is similar to what occurs with Freebase.Topic. Getting back the results as a map is all fine and well but if I’m making the query for very specific reason I’d prefer that it be parsed into a POJO that lets me directly access the data I need. This can be done but it takes a little work.

I created the Person class to hold values from Freebase’s /people/person topic. I want the JSON parser to populate the children string array with the children values returned from the query.

public class Person {
 @com.google.api.client.util.Key
 private String[] children;

 public String[] getChildren() {
  return children;
 }

 public void setChildren(String[] children) {
  this.children = children;
 }
}

I created SingleMQLReadPersonResponse to hold envelop returned by the query. The result field is typed as Person so the JSON parser should populate it accordingly.


public class SingleMQLReadPersonResponse extends GenericJson {
 @com.google.api.client.util.Key
 private Person result;

 public Person getResult() {
 return result;
 }

 public void setResult(Person result) {
 this.result = result;
 }
 }

<span style="font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 18px;">

With the following code I can directly access the person’s children.


Freebase.Mqlread mqlRead = freebase.mqlread(query, SingleMQLReadPersonResponse.class);
 SingleMQLReadPersonResponse response = mqlRead.execute();
 System.out.println(response.toPrettyString());
 Person person = response.getResult();
 System.out.println(person);
 for (String child : person.getChildren()) {
 System.out.println(child);
 }

Here’s the output …

{
"result" : {
"children" : [ "Eduard Einstein", "Hans Albert Einstein", "Lieserl Einstein", "Ilse Einstein", "Margot Einstein" ]
}
}
org.bwgz.freebase.sample.MQLReadTest$Person@3aa59d
Eduard Einstein
Hans Albert Einstein
Lieserl Einstein
Ilse Einstein
Margot Einstein

So far so good. But I don’t want to create a response envelop class for every query I plan to make. I’d rather use Java generics to create a single class that can be reused. Something like this …


public static class SingleMQLReadResponse extends GenericJson {
 @com.google.api.client.util.Key
 private T result;

 public T getResult() {
 return result;
 }

 public void setResult(T result) {
 this.result = result;
 }
 }

Here’s the code that uses it.


Freebase.Mqlread<SingleMQLReadResponse> mqlRead = freebase.mqlread(query, SingleMQLReadResponse.class);
 SingleMQLReadResponse response = mqlRead.execute();
 System.out.println(response.toPrettyString());
 Person person = response.getResult();
 System.out.println(person);

But there a problem.  Here’s the output …

{
"result" : {
"id" : "/en/albert_einstein",
"type" : "/people/person",
"children" : [ "Eduard Einstein", "Hans Albert Einstein", "Lieserl Einstein", "Ilse Einstein", "Margot Einstein" ]
}
}
java.lang.ClassCastException: com.google.api.client.util.ArrayMap cannot be cast to org.bwgz.freebase.sample.MQLReadTest$Person
at org.bwgz.freebase.sample.MQLReadTest.SingleResponseTest2(MQLReadTest.java:101)
at org.bwgz.freebase.sample.MQLReadTest.main(MQLReadTest.java:123)

While SingleMQLReadResponse has been typed to Person the mqlread method is only passed SingleMQLReadResponse.class. Java does not allow SingleMQLReadResponse<T>.class. The parser cannot determine the result field’s class (type) and it falls back to com.google.api.client.util.ArrayMap. That in turn causes the cast exception.

Luckily there a way around this. The underlying code will use the class’s type (Type) when converting JSON to a Java object. If SingleMQLReadResponse.class is passed it’s parameter types are not included. This wasn’t a problem when SingleMQLReadPersonResponse.class was passed because the result field as explicitly typed to Person. The underlying code will accept a Type instead of a Class. If we can pass in a ParameterizedType the parser will have the information it needs to convert the JSON results to the specified Java object. There are two things we need:

  • Create a ParameterizedType representing the response class we are using.
  • Get the underlying code to use Type rather than Class.

Java contains the ParameterizedTypeImpl class which will produce a ParameterizedType object. Unfortunately it is protected and can’t be accessed. Rather than write my own I just grabbed the source code and included it in my project. The code isn’t complicated and I could have written my own but why bother when it’s freely available. The following code creates the Type object I need and takes create of the first thing I needed.

ParameterizedTypeImpl.make(SingleMQLReadResponse.class, new Type[] { Person.class }, null));

The execute method is located in AbstractGoogleClientRequest which is a deeply nested superclass of Mqlread. Here’s the code for execute.


public T execute() throws IOException {
 HttpResponse response = executeUnparsed();
 // TODO(yanivi): remove workaround when feature is implemented
 // workaround for http://code.google.com/p/google-http-java-client/issues/detail?id=110
 if (Void.class.equals(responseClass)) {
 response.ignore();
 return null;
 }
 return response.parseAs(responseClass);
 }

This code parses the HTTP response using the class (responseClass) which was passed through when Mqlread was constructed. HttpResponse.parse will accept either a Class or a Type. I don’t want to mess with AbstractGoogleClientRequest but I’ve already got my mitts into Freebase.Mqlread and there are changes I can make there.

First, create a second constructor with accepts a Type and saves it.

protected Mqlread(String query, Class responseClass) {
 super(Freebase.this, "GET", REST_PATH, null, responseClass);
 this.query = Preconditions.checkNotNull(query, "Required parameter query must be specified.");
 }

// Here's the second constructor.

protected Mqlread(String query, Class responseClass, Type responseType) {
 this(query, responseClass);
 this.responseType = responseType;
 }

I still have to pass along responseClass even though it won’t be used. If I don’t the underlying code will throw a validation exception.

Second, I override the execute method.

private T execute(Type type) throws IOException {
 HttpResponse response = executeUnparsed();

 return (T) response.parseAs(type);
 }

@Override
 public T execute() throws IOException {
 return (getResponseType() != null) ? execute(getResponseType()) : super.execute();
 }

Now we I run this code everything is parsed as expected and get the same results as with SingleMQLReadPersonResponse.

Type type = ParameterizedTypeImpl.make(SingleMQLReadResponse.class, new Type[] { Person.class }, null);

Freebase.Mqlread<SingleMQLReadResponse> mqlRead = freebase.mqlread(query, SingleMQLReadResponse.class, type);
 SingleMQLReadResponse response = mqlRead.execute();
 System.out.println(response.toPrettyString());
 Person person = response.getResult();
 System.out.println(person);
 for (String child : person.getChildren()) {
 System.out.println(child);
 }

{
"result" : {
"children" : [ "Eduard Einstein", "Hans Albert Einstein", "Lieserl Einstein", "Ilse Einstein", "Margot Einstein" ]
}
}
org.bwgz.freebase.sample.MQLReadTest$Person@b3826f
Eduard Einstein
Hans Albert Einstein
Lieserl Einstein
Ilse Einstein
Margot Einstein

Some final thoughts.

  • When Mqlread is completed classes like SingleMQLReadResponse will likely exist in com.google.api.services.freebase.model.
  • The Google Client API should support Type along with Class parsing. When that occurs Mqlread can more easily support parameterized types.
  • Error handling needs to be considered. If the response returns a JSON envelop with an error message then there’s a need to easily capture and use it.

The source code is available at GitHub.

Categories: Freebase, Java

Quotation Content Provider

March 23, 2013 Leave a comment

It’s time to have my quotation content provider to take direct advantage of Freebase’s . Here are a list of provider requirements:

  1. It must always have content to provide. It cannot rely on fetching content via a network. It therefore must have some initialization data when the application is installed. 
  2. As quotations are consumed the provider will replace those quotations with new ones from Freebase.

This iteration will implement the initial connections to Freebase. Later version will refine it.

The provider will use Android’s SQLite to cache the content. I’ll start out with one table that holds the quotation, author’s name, and URL to author’s image. Later, as more data is needed, I’ll add additional tables and normalize the model.

After taking a look at android.provider.ContactsContract I decided to mimic this pattern. QuotationContract describes the  quotation table and how to use a Uri to reference a row in the table.

Every quotation in Freebase has a unique id so we’ll use that id as the primary key in the table. This means the Uri for a quotation is the authority Uri plus the quotation table name plus the free base id. This Uri becomes the standard way to identify a quotation. It will be used throughout the code. For example …

content://org.bwgz.quotation/quotation/quotationsbook/quote/8185

references a quotation from Ralph Waldo Emerson.

A foolish consistency is the hobgoblin of little minds, adored by little statesmen and philosophers and divines.

public final class QuotationContract {
	public static final String AUTHORITY = "org.bwgz.quotation";
	public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);

	protected interface StatusColumns {
		public enum State {
			UNINITIALIZED(0), INITIALIZED(1);

		    private final int value;
		    State(int value) {
		        this.value = value;
		    }
		    public int getValue() {
		    	return value;
		    }
		}

		public static final String STATE		= "state";
		public static final String MODIFIED		= "modified";
	}

	protected interface QuotationColumns {
		public static final String QUOTATION	= "quotation";
		public static final String AUTHOR_NAME	= "author_name";
		public static final String AUTHOR_IMAGE	= "author_image";
	}

	public static class Quotation implements BaseColumns, StatusColumns, QuotationColumns {
		public static final String TABLE = "quotation";

		private Quotation() {
		}

		public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE);

		public static Uri getUri(String id) {
			return Uri.parse(CONTENT_URI + id);
		}

		public static String getId(Uri uri) {
	    	return uri.getPath().substring(Quotation.CONTENT_URI.getPath().length(), uri.getPath().length());
		}

		/**
		 * The MIME type of {@link #CONTENT_URI} providing a directory of
		 * quotations.
		 */
		public static final String CONTENT_TYPE = "vnd.android.cursor.dir/quotation";

		/**
		 * The MIME type of a {@link #CONTENT_URI} a single quotation.
		 */
		public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/quotation";

	}
}

QuotationSQLiteHelper, an extension of SQLiteOpenHelper, provides methods to create the database, initialize it, and CRUD operations.

QuotationSQLiteHelper, an extension of SQLiteOpenHelper, provides methods to create the database, initialize it, and CRUD operations. When the database is created it is also initializes the quotation table from a CSV file containing quotes and their Freebase id.  The author name and image URL are not initialized. We’ll use a lazy fetch to get those when needed. The state column is set to uninitialized because those fields aren’t set.

QuotationSyncAdapter.onPerformSync now expects to be passed the Uri of quotation requiring synchronization. It parses the Freebase quotation id from the Uri, builds a Freebase query, and executes it. If the query is successful it will update the table using a batch operation that calls the provider’s update method. If that update is successful the provider will notify any objects that had requested notification of changes to that Uri.

	private MQLQueryBuilder builder = new MQLQueryBuilder();

	private Quotation findQuotationFromFreebase(String id) {
		Quotation quotation = new Quotation();

		quotation.setId(id);
		String query = builder.createQuery(Quotation.class, null, quotation);
		Log.d(TAG, String.format("query: %s", query));

		FreebaseQuery fbQuery = new FreebaseQuery();
		org.bwgz.freebase.model.Quotation result = fbQuery.getResult(query, Quotation.class);
		Log.d(TAG, String.format("result: %s\n", result));

		return result;
	}

	@Override
	public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
		Log.d(TAG, String.format("onPerformSync - account: %s  extras: %s  authority: %s  provider: %s  syncResult: %s", account, extras, authority, provider, syncResult));

		for (String key : extras.keySet()) {
			Log.d(TAG, String.format("%s: %s", key, extras.get(key)));
		}

		String string = extras.getString(SYNC_EXTRAS_QUOTATION_UPDATE);
		if (string != null) {
			Uri uri = Uri.parse(string);
	    	String _id = org.bwgz.quotation.content.provider.QuotationContract.Quotation.getId(uri);
			Log.d(TAG, String.format("_id: %s", _id));

			Quotation quotation = findQuotationFromFreebase(_id);
			if (quotation != null) {
				ArrayList operationList = new ArrayList();

				ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(uri);
				builder.withValue(org.bwgz.quotation.content.provider.QuotationContract.Quotation.QUOTATION, quotation.getName());
				builder.withValue(org.bwgz.quotation.content.provider.QuotationContract.Quotation.AUTHOR_NAME, quotation.getAuthor().getName());
				builder.withValue(org.bwgz.quotation.content.provider.QuotationContract.Quotation.AUTHOR_IMAGE, quotation.getAuthor().getId());
				builder.withSelection("_id = ?", new String[] { _id });
				operationList.add(builder.build());

				try {
					getContext().getContentResolver().applyBatch(QuotationContract.AUTHORITY, operationList);
				} catch (RemoteException e) {
					e.printStackTrace();
				} catch (OperationApplicationException e) {
					e.printStackTrace();
				}
			}
		}
	}

When QuoteActivity resumes it determine which quotation, using the quotation’s Uri, it should be displaying. If the quotation has not be initialized it will:

  1. Display a “waiting” message.
  2. Register a ContentObserver for the Uri.
  3. Request that the content provider update (sync) the quotation.

The sync request is non-blocking. The onResume method returns and the application continues to run normally. Meanwhile, the sync request runs in the background and if successful the ContentObserver (QuotationContentObserver) will be notified by the provider when the table has been updated. QuotationContentObserver then fetches the quotation from the database and updates the display accordingly.

	class QuotationContentObserver extends ContentObserver {
		private Uri uri;

		public QuotationContentObserver() {
	        super(new Handler());
	    }

		public Uri getUri() {
			return uri;
		}

		public void setUri(Uri uri) {
			this.uri = uri;
		}

	    @Override
	    public void onChange(boolean selfChange) {
	        super.onChange(selfChange);
	        Log.d(TAG, "QuoteContentObserver.onChange( " + selfChange + ")");

	        String quotation = getQuotation(uri);
	        if (quotation != null) {
	        	setQuote(quotation);
	        }

	        String author = getAuthor(uri);
	        if (author != null) {
	        	setAuthor(author);
	        }

	        String authorImage = getAuthorImage(uri);
	        if (authorImage != null) {
	        	setAuthorImage(authorImage);
	        }
	    }
	}

    ...

    protected void onResume() {
    	super.onResume();
		Log.d(TAG, String.format("onResume"));

		Intent intent = getIntent();
		Uri uri = intent.getData();
		Log.d(TAG, String.format("intent: %s  uri: %s", intent, uri));

		if (uri == null) {
			uri = getRandomQuotationUri();
			Log.d(TAG, String.format("random uri: %s", uri));
		}

		if (isQuotationInitialized(uri)) {
			setQuote(getQuotation(uri));
			setAuthor(getAuthor(uri));
	        setAuthorImage(getAuthorImage(uri));
		}
		else {
			quotationObserver.setUri(uri);
			getContentResolver().registerContentObserver(uri, false, quotationObserver);

			setQuote("Waiting for quotation to load ...");
			setAuthor("");

			Bundle extras = new Bundle();
	        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
	        extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false);
	        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
	        extras.putString(QuotationSyncAdapter.SYNC_EXTRAS_QUOTATION_UPDATE, uri.toString());
			ContentResolver.requestSync(new QuotationAccount(), QuotationContract.AUTHORITY, extras);
		}
    }

The quote widget works similarly. Now the user clicks on the quote widget it will send an intent containing the quote’s Uri to the quote activity. The activity provides further details such as the author’s name and image.

device-2013-03-22-220229 device-2013-03-22-220213

Freebase also provides an image service. The application fetches the author’s image rather than trying to store it. The fetch is done in the background using an AsyncTask.

public class QuoteActivity extends Activity {

        ...

	public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
	    ImageView bmImage;

	    public DownloadImageTask(ImageView bmImage) {
	        this.bmImage = bmImage;
	    }

	    protected Bitmap doInBackground(String... urls) {
	        String urldisplay = urls[0];
	        Bitmap bitmap = null;
	        try {
	            InputStream in = new java.net.URL(urldisplay).openStream();
	            bitmap = BitmapFactory.decodeStream(in);
	        } catch (Exception e) {
	            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
	        }
	        return bitmap;
	    }

	    protected void onPostExecute(Bitmap result) {
	        bmImage.setImageBitmap(result);
	    }
	}

        ...

	public void setAuthorImage(String image) {
		Log.d(TAG, String.format("setAuthorImage - image: %s", image));
		ImageView imageView = (ImageView) findViewById(R.id.image);
		Uri uri = Uri.parse("https://usercontent.googleapis.com/freebase/v1/image" + image + "?maxwidth=200&maxheight=200&pad=true");
		Log.d(TAG, String.format("setAuthorImage - uri: %s", uri));
		new DownloadImageTask(imageView).execute(uri.toString());
	}

       ....
}

Working with Freebase – Part 3

February 21, 2013 Leave a comment

My next step with Freebase was to generalize the way I queried it. I decided to model my API after the Java Persistence API (JPA) which provides a POJO persistence model for object-relational mapping. In this case its an object-structured data mapping. The Freebase graph is a set of nodes and a set of links or relationships between those nodes. My API will provide a straight forward way to retrieve those nodes and relationships in the form of a POJO. This post will describe my initial efforts.

In its most primitive form a Freebase node has a unique identifier, creation time stamp, and creator. Properties specify a relationship. That can be a relationship with another node or primitive value. Unique properties have only one relationship to a node/value. Non-unique properties can have many.  There are also universal properties (name, key, type, permission, mid). Primitive values include int, float, boolean, text, rawstring, uri, datetime, key, and id.

JPA describes an entity as …

An entity is a lightweight persistence domain object. Typically an entity represents a table in a relational database, and each entity instance corresponds to a row in that table. The primary programming artifact of an entity is the entity class, although entities can use helper classes.

The persistent state of an entity is represented either through persistent fields or persistent properties. These fields or properties use object/relational mapping annotations to map the entities and entity relationships to the relational data in the underlying data store.

I will reuse this concept of entity to represent a Freebase node. For example, the following class represents a node of type /people/person.

@FBEntity(type = "/people/person")
public class Person implements Serializable {

	@FBProperty(property_value = "/people/person")
	private String type;
	@FBProperty
	private String id;
	@FBProperty
	private String name;

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

Consider the simplest of queries – select all from. In SQL this would look like SELECT * FROM name. MQL does not have a true equivalent.  The following MQL would select type, id, name from node type /people/person.

[{
 "type": "/people/person",
 "id": null,
 "name": null
}]

The code for executing this query looks like this.


public class QueryTestOne {
	private static final String personQuery =
			"[{"						+
				  "\"type\": \"/people/person\","	+
				  "\"id\": null,"			+
				  "\"name\": null"			+
				  "}]";

	public static  void dump(List list) {
		if (list != null) {
			for (T result : list) {
				System.out.printf("result: %s\n", result);
			}
		}
	}

	public static void test1(EntityManager em, String query) {
		Query q = em.createQuery(query);
		List<?> list = q.getResultList();
		dump(list);
	}

	public static void test2(EntityManager em, String query, Class<?> clazz) {
		Query q = em.createQuery(query, clazz);
		List<?> list = q.getResultList();
		dump(list);
	}

	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("https", "www.googleapis.com", "/freebase/v1/mqlread/", null);
		EntityManager em = emf.createEntityManager();

		test1(em, personQuery);
		test2(em, personQuery, Person.class);
	}
}

The Freebase read API will return the results in a JSON response envelop that uses the following general form.

{
  "result": [{ MQL Response Here }],
  "status": "200 OK",
  "code": "/api/status/ok",
  "transaction_id":[opaque string value]
}

result contains the body of results and can either be {} (singleton) or [] array. My API will always use an array. I created  a class MQLMultipleResultResponse to hold the response envelop. For now I’m ignoring the other items such as status.

public class MQLMultipleResultResponse {
	private T[] result;

	public T[] getResult() {
		return result;
	}
	public void setResult(T[] result) {
		this.result = result;
	}
}

The query can be executed with or without a type. If the type is absent the underlying code will use Object. The type defines what kind of of object array will be returned. For example, if Person.class is specified an array of Person will be returned.

The underlying code executes the following …

URI uri = new URI(entityManager.getScheme(), entityManager.getAuthority(), entityManager.getPath(), Util.createQuery(variables), null);

ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET);
ClientHttpResponse response = request.execute();
ObjectMapper objectMapper = new ObjectMapper();
MQLMultipleResultResponse body = objectMapper.readValue(response.getBody(), typeReference);
results = body.getResult();

Most of this is straight forward HTTP client and JSON calls. The use of objectMapper.readValue() is somewhat unique. Since MQLMultipleResultResponse uses a generic I initially ran into problems with the following …

objectMapper.readValue(response.getBody(), MQLMultipleResultResponse.class);

objectMapper.readValue() cannot know what type the result body will be so it defaults to a Map. The following isn’t legal …

objectMapper.readValue(response.getBody(), MQLMultipleResultResponse.class);

Fortunately Jackson provides an alternate. Instead of passing the Class you can pass a TypeReference. The following code creates the necessary TypeReference …

TypeReference<MQLMultipleResultResponse> typeReference =
  new TypeReference<MQLMultipleResultResponse>() {
    public Type getType() {
      return ParameterizedTypeImpl.make(MQLMultipleResultResponse.class, new Type[] { resultClass }, null    );
  }
};

The key is providing the correct Type. In this case the subclass ParameterizedType. ParameterizedTypeImpl implements ParameterizedType. Java contains a version of ParameterizedTypeImpl but it’s inaccessible. Implementing ParameterizedType is just complicated enough that I decided to just copy ParameterizedTypeImpl’s source code into my project. It comes with a helpful make() method.

With all this in place the test client generates the following results. In one case an array of objects and in the other an array of Person.

result: {name=Dan Milbrath, type=/people/person, id=/en/dan_milbrath}
result: {name=David Safavian, type=/people/person, id=/en/david_safavian}
result: {name=Robert Cook, type=/people/person, id=/en/robert_cook}

result: org.bwgz.freebase.model.Person@2a444: getName: Dan Milbrath; getId: /en/dan_milbrath; getType: /people/person; getClass: class org.bwgz.freebase.model.Person; }
result: org.bwgz.freebase.model.Person@c126b3: getName: David Safavian; getId: /en/david_safavian; getType: /people/person; getClass: class org.bwgz.freebase.model.Person; }
result: org.bwgz.freebase.model.Person@1c01b97: getName: Robert Cook; getId: /en/robert_cook; getType: /people/person; getClass: class org.bwgz.freebase.model.Person; }
Categories: Freebase, Java, JPA, Structured Data

Working with Freebase – Part 2

February 14, 2013 Leave a comment

In part 1 I demonstrated a method to generate a Freebase MQL query from an annotated class that is intended to mimic JPA’s ORM behavior. Next I’ll show how to use the Spring RestTemplate to execute the query and create an array of objects from the results. The following code fragment illustrates the steps.

RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJacksonHttpMessageConverter());

URI uri = new URI("https", "www.googleapis.com", "/freebase/v1/mqlread/", Util.createQuery(variables), null);
ResponseEntity entity = restTemplate.getForEntity(uri, PersonsResponse.class);
PersonsResponse response = entity.getBody();
Person[] persons = response.getResult();

The first step is the create a RestTemplate and then add a JSON message converter (MappingJacksonHttpMessageConverter) to it. Freebase returns the its results in JSON format. An array of results will look like this …

{
"code":          "/api/status/ok",
"result": [
{
"type": "/people/person",
"id": "/en/dan_milbrath",
"gender": {
"type": "/people/gender",
"id": "/en/male",
"name": "Male"
},
"name": "Dan Milbrath"
},
{
"type": "/people/person",
"id": "/en/david_safavian",
"gender": {
"type": "/people/gender",
"id": "/en/male",
"name": "Male"
},
"name": "David Safavian"
},
{
"type": "/people/person",
"id": "/en/robert_cook",
"gender": {
"type": "/people/gender",
"id": "/en/male",
"name": "Male"
},
"name": "Robert Cook"
}
],
"status":        "200 OK",
"transaction_id": "cache;cache02.p01.sjc1:8101;2013-02-14T07:31:07Z;0064"
}

ResponseEntity holds the code, status, and transaction_id properties. Typing it to an appropriate class allows it to hold the result returned in the JSON format. In this case it is PersonsResponse which is a subclass of MQLMultipleResultResponse typed to Person.

class PersonsResponse extends MQLMultipleResultResponse<Person>  {
}

public class MQLMultipleResultResponse<T> {
	private String cursor;
	private T[] result;

	public T[] getResult() {
		return result;
	}
	public void setResult(T[] result) {
		this.result = result;
	}
	public String getCursor() {
		return cursor;
	}
	public void setCursor(String cursor) {
		this.cursor = cursor;
	}
}

The code is available at GitHub.

Categories: Bruce's Posts, Java, Spring

Working with Freebase – Part 1

February 14, 2013 Leave a comment

Freebase provides a remote read service API for accessing Freebase database using the Metaweb query language (MQL). Using an HTTP endpoint an application can send a MQL query and received a result set in the response. The result set is returned in the JSON (JavaScript Object Notation) format.

In my case I wanted to query the database for quotes. For example, return quotations from the database. The following MQL returns all quotations associated with a person in the database.

[{
"type": "/people/person",
"id": null,
"name": null,
"/people/person/quotations": [{
"type": "/media_common/quotation",
"id": null,
"name": null,
}]
}]

This produces a results similar to this:

{
"code":          "/api/status/ok",
"result": [
{
"/people/person/quotations": [
{
"id":   "/en/first_thing_we_do_lets_kill_all_the_lawyers",
"name": "First thing we do, let's kill all the lawyers.",
"type": "/media_common/quotation"
},
{
"id":   "/en/o_brave_new_world_that_has_such_people_int",
"name": "...O brave new world, That has such people in't!",
"type": "/media_common/quotation"
}
],
"id":   "/en/william_shakespeare",
"name": "William Shakespeare",
"type": "/people/person"
},
{
"/people/person/quotations": [
{
"id":   "/m/02hylj7",
"name": "...the shifty, hangdog look which announces that an Englishman is about to talk French.",
"type": "/media_common/quotation"
},
{
"id":   "/en/routine_is_the_death_to_heroism",
"name": "Routine is the death to heroism.",
"type": "/media_common/quotation"
}
],
"id":   "/en/p_g_wodehouse",
"name": "P. G. Wodehouse",
"type": "/people/person"
}
],
"status":        "200 OK",
"transaction_id": "cache;cache02.p01.sjc1:8101;2013-02-14T07:31:07Z;0064"
}

In addition to executing the query I want the code to follow an ORM (Object Relational Mapping) style pattern. The code should know how to build the query, execute it, and return the results as objects. I used JPA annotations as a model for creating the mappings.

I created two annotations:

  • FBEntity – Describes which Freebase resource type the class is associated with.
  • FBProperty – Describes which Freebase property the method or field is associated with.

Using those annotations I create the following classes to represent a person and gender.

Person.java

@FBEntity(type = "/people/person")
public class Person {

	@FBProperty(property_value = "/people/person")
	private String type;
	@FBProperty
	private String id;
	@FBProperty
	private String name;
	@FBProperty
	private Gender gender;

	public String getType() {
		return type;
	}
	public void setType(String type) {
		this.type = type;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	public Gender getGender() {
		return gender;
	}
	public void setGender(Gender gender) {
		this.gender = gender;
	}
}

Gender.java

@FBEntity(type = "/people/gender")
public class Gender {

	@FBProperty(property_value = "/people/gender")
	private String type;
	@FBProperty
	private String id;
	@FBProperty
	private String name;

	public String getType() {
		return type;
	}
	public void setType(String type) {
		this.type = type;
	}
	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

A query builder class uses the annotations to create a MQL query. This is where all the heavy lifting occurs. The builder will parse the class and for each field/method with an annotation it will generate the necessary MQL. It accepts special directives such as limit which are associated with a class. These directives can be used to control the behavior of the query.  It also accepts an instance of the object being queried and use it’s values to refine the query.

MQLQueryBuilder.java

public class MQLQueryBuilder {
	static private final String OPEN_SQUARE = "[";
	static private final String CLOSE_SQUARE = "]";
	static private final String OPEN_BRACKET = "{";
	static private final String CLOSE_BRACKET = "}";
	static private final String EMPTY = "";
	static private final String SPACE = " ";
	static private final String QUOTE = "\"";
	static private final String COMMA = ",";
	static private final String NEW_LINE = "\n";

	static public int PROPERTY_COMPACT	= 0;
	static public int PROPERTY_PRETTY	= 1;

	private String space = EMPTY;
	private String new_line = EMPTY;

	public MQLQueryBuilder(int property) {
		if (property == PROPERTY_COMPACT) {
			space = EMPTY;
			new_line = EMPTY;
		}
		else if (property == PROPERTY_PRETTY) {
			space = SPACE;
			new_line = NEW_LINE;
		}
	}

	public MQLQueryBuilder() {
		this(PROPERTY_COMPACT);
	}

	private String indent(int tab) {
		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < tab * 2; i++) {
			buffer.append(space);
		}
		return buffer.toString();
	}

	private String quote(String string) {
		return new StringBuffer().append(QUOTE).append(string).append(QUOTE).toString();
	}

	private Object getValueFromObject(Class<?> clazz, Object object, Field field) {
		//System.out.printf("clazz: %s  object: %s  field: %s\n", clazz, object, field);
		Object value = null;
		String name = "get".concat(field.getName()).toLowerCase();

		Method method = null;

		for (Method m : clazz.getMethods()) {
			if (m.getName().toLowerCase().equals(name)) {
				try {
					method = clazz.getMethod(m.getName());
				} catch (NoSuchMethodException e) {
					e.printStackTrace();
				} catch (SecurityException e) {
					e.printStackTrace();
				}
				break;
			}
		}

		if (method != null) {
			try {
				value = method.invoke(object);
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}

		return value;
	}

	private String createQuery(Class<?> clazz, Map<Class<?>, MQLProperty[]> map, Object object, Stack<Class<?>> stack, int tab) {
		StringBuffer sb = new StringBuffer();
		String indent = indent(tab);

		if (clazz.isArray()) {
			sb.append(String.format("%s", OPEN_SQUARE));
			sb.append(createQuery(clazz.getComponentType(), map, object, stack, tab + 1));
			sb.append(String.format("%s", CLOSE_SQUARE));
		}
		else {
			FBEntity fbEntity = clazz.getAnnotation(FBEntity.class);

			if (fbEntity != null) {
				stack.push(clazz);
				boolean next = false;
				sb.append(String.format("%s%s", OPEN_BRACKET, new_line));

				for (Field field : clazz.getDeclaredFields()) {
					Class<?> type = field.getType();
					if (!stack.contains(type.isArray() ? type.getComponentType() : type)) {
						FBProperty fbProperty = field.getAnnotation(FBProperty.class);
						if (fbProperty != null) {
							String name = quote((fbProperty.property_name().length() != 0) ? fbProperty.property_name() : field.getName());
							String value = null;

							if (type.isArray()) {
								value = createQuery(type, map, object != null ? getValueFromObject(clazz, object, field) : null, stack, tab + 1);
							}
							else {
								fbEntity = type.getAnnotation(FBEntity.class);
								if (fbEntity != null) {
									value = createQuery(type, map, object != null ? getValueFromObject(clazz, object, field) : null, stack, tab + 1);
								}
								else {
									if (object != null) {
										Object o = getValueFromObject(clazz, object, field);
										if (o != null) {
											value = quote(o.toString());
										}
									}

									if (value == null) {
										value = fbProperty.property_value().length() != 0 ? quote(fbProperty.property_value()) : null;
									}
								}
							}

							sb.append(String.format("%s%s%s:%s%s", next ? (COMMA + new_line) : EMPTY, indent, name, space, value));
							next = true;
						}
					}
				}

				if (map != null) {
					MQLProperty[] directives = map.get(clazz);
					if (directives != null) {
						for (MQLProperty directive : directives) {
							String name = quote(directive.getName());
							String value = directive.getValue() != null ? directive.getValue() instanceof String ? quote(directive.getValue().toString()) : directive.getValue().toString() : null;
							sb.append(String.format("%s%s%s:%s%s", next ? (COMMA + new_line) : EMPTY, indent, name, space, value));
							next = true;
						}
					}
				}

				sb.append(String.format("%s%s%s", new_line, indent, CLOSE_BRACKET));
				stack.pop();
			}
		}

		return sb.toString();
	}

	public String createQuery(Class<?> clazz, Map<Class<?>, MQLProperty[]> map, Object object) {
		return createQuery(clazz, map, object, new Stack<Class<?>>(), 0);
	}

	public String createQuery(Class<?> clazz, Map<Class<?>, MQLProperty[]> map) {
		return createQuery(clazz, map, null);
	}

	public String createQuery(Class<?> clazz) {
		return createQuery(clazz, null);
	}
}

The following test program will produces two queries. One will return all persons and the other only males. The limit directive limits the query to just three results. Passing an instance of Person with the Gender set to “Male” will refine the results to only males.

PersonTestOne.java

public class PersonTestOne {

	public static void main(String[] args) {
		Map<Class<?>, MQLProperty[]> map = new HashMap<Class<?>, MQLProperty[]>();

		MQLProperty[] directives = new MQLProperty[] {
				new MQLProperty("limit", new Integer(3))
		};
		map.put(Person.class, directives);

		MQLQueryBuilder builder = new MQLQueryBuilder(
				MQLQueryBuilder.PROPERTY_PRETTY);
		String mql = builder.createQuery(Person[].class, map);
		System.out.println(mql);

		Gender gender = new Gender();
		gender.setName("Male");
		Person person = new Person();
		person.setGender(gender);
		mql = builder.createQuery(Person[].class, map, person);
		System.out.println(mql);
	}
}

Running the program generates the following MQL.

[{
 "type": "/people/person",
 "id": null,
 "name": null,
 "gender": {
 "type": "/people/gender",
   "id": null,
   "name": null
   },
 "limit": 3
 }]

[{
 "type": "/people/person",
 "id": null,
 "name": null,
 "gender": {
 "type": "/people/gender",
   "id": null,
   "name": "Male"
   },
 "limit": 3
 }]

The next step is to execute the query in a manner that populates an array of objects with the results.

Categories: Bruce's Posts
Follow

Get every new post delivered to your Inbox.

Join 48 other followers