Sunday, December 1, 2013

Library to make null-safe collections in thrift generated java object.


While I was experimenting evernote java API, I found thrift generated code for container types(list,set,map) are not really java friendly, especially for for-loop.
In thrift, when value is set to null, the transport will not send the field. But in java, when collection is null, the enhanced-for-loop throws NullPointerException.
Note note = noteStore.getNote(...);
// This throws NPE when the note doesn't have any resources
for (Resource resource : note.getResources()) {
...
}

I can put if-statement to check the collection whenever I need to access it, but that's just repetitive...

So, I wrote a util class that wraps thrift generated class to make it null-safe.

* w() is a static method in ThriftWrapper class which I wrote.
Note wrapped = w(new Note());  // wrapped
for (Resource resource : wrapped.getResources()) {
  // no more NPE. nested thrift attributes are also null-safe
}

Mechanism:

The wrapping method introspect the given thrift object and set empty mutable collections(ArrayList, HashSet, HashMap) when it finds null in collection fields. Also, it traverse child thrift classes and set empty collections as well.(foo.getBar().getBaz().getList()).
Then it returns cglib generated proxy. Since in thrift, null has a meaning when it serializes object, the proxy keeps track of collection fields which were initially null, and when "write()" method is called, it compares the current value and if it's still empty, then it will omit the field to transport.

wrapping behavior:
Note note = new Note();
note.setAttributes(new NoteAttributes());
assertThat(note.getTagGuids(), is(nullValue()));
assertThat(note.getAttributes().getClassifications(), is(nullValue()));

Note wrapped = w(note);
assertThat(wrapped.getTagGuids(), is(emptyCollectionOf(String.class)));
assertThat(wrapped.getAttributes().getClassifications(), is(notNullValue()));
assertThat(wrapped.getAttributes().getClassifications().size(), is(0));
transport behavior:
Note note = new Note();
Note wrapped = w(note);  // still a note instance  (actually a cglib proxy)
wrapped.getTagGuids().size();  // 0 (empty list)
wrapped.getResources().add(...);  // add something to collection

noteStore.createNote(wrapped);  // internally call wrapped.write(...)
// added resources are included in message, but tag-guids are not

Please reference more detailed behavior in test classes.


Code:

I pushed to my github repo.


Further:

Since I'm new to thrift, it might not be a good approach or there might be a better solution already.
If there is more request for this approach, I'll further enhance the project such:

  • inline cglib to avoid dependency conflict
  • make the class more spring-framework friendly bean instead of static methods
  • release and push to public maven repo
  • etc.