Thursday, May 6, 2010

Converting Google Protocol Buffers To JSON Objects and vice versa in Java

I am working on an application and experimenting with Google Protocol Buffers.  The idea is the back end service communication will use Protocol Buffers but I have some components which will be accessed by a service on the web.

I searched around on Google and found a few references from Google engineers stating that it would not be difficult to convert Protocol Buffers to JSON or the other way around as well as references hinting that this is how Google Maps works but I could not find any code for it.

I did find a JavaScript library that is supposed to encode and decode the binary format but it did not really make sense to me to go this route on the front end since the HTTP request still has a lot of text in it and the savings of binary encoding for most HTTP requests seems minimal.  Anyway JavaScript does not really seem to be suited for reading and writing binary protocols.  Add to that the fact that the library did not even have solid release and it was time to roll my own converter.

Mainly I just wanted to reuse the object models I create in my .proto files on the front end with those I use on the back end and this was the best way I could think of to do this.  I have included below the code I came up with which works for my needs.  I did not implement for all cases but if it is useful to your project the code is all there to improve on.

I should mention this uses a library for Java from "JSON.org".  The models were just generated from my .proto files using protoc.  

Anyway without further ado here is the code.  If you make any improvements on this or have ideas on how to improve it please let me know.  One other word of explanation the protocol class here if for my application the important part is the "decodeMessage" and "encodeMessage" part so I have removed some of the irrelevant parts from this.


package hak.server3.protocol;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import com.google.protobuf.Message;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.json.JSONException;
import org.json.JSONObject;

import hak.server3.Protocol;

/**
 * This protocol reads and writes JSON it is primarily meant for communication
 * between browsers and internal services.
 * 
 * @author ben hakala
 */
public class JSONProtocol implements Protocol {
// This is from JSONObject but is not visible os I am putting it here
static boolean isStandardProperty(Class clazz) {
        return clazz.isPrimitive()                  ||
            clazz.isAssignableFrom(Byte.class)      ||
            clazz.isAssignableFrom(Short.class)     ||
            clazz.isAssignableFrom(Integer.class)   ||
            clazz.isAssignableFrom(Long.class)      ||
            clazz.isAssignableFrom(Float.class)     ||
            clazz.isAssignableFrom(Double.class)    ||
            clazz.isAssignableFrom(Character.class) ||
            clazz.isAssignableFrom(String.class)    ||
            clazz.isAssignableFrom(Boolean.class);
    }
public static Message decodeMessage(JSONObject obj) throws JSONException {
String type = obj.getString("_pb_class");
if(type == null) {
throw new JSONException("Cannot decode message, missing _pb_class");
}
Message msg = null;
try {
// Create an instance of this class and set up its properties
Class cls = Class.forName(type);
Method m = cls.getDeclaredMethod("newBuilder", (Class[])null);
Object builder = (Object) m.invoke(null, null);
// now go ahead and put this message together
Iterator iter = obj.keys();
Class bcls = builder.getClass();
Method[] methods = bcls.getDeclaredMethods();
while(iter.hasNext()) {
String key = iter.next();
if( ! key.equals("_pb_class")) {
String methnm = "set";
String[] parts = key.split("_");
for(int i=0; i < parts.length; i++) {
if(parts[i].length() > 0) {
//System.out.println("parts["+i+"] is '"+parts[i]+"'");
String fc = parts[i].substring(0,1).toUpperCase();
parts[i] = parts[i].toLowerCase().substring(1,parts[i].length());
methnm += fc+parts[i];
}
}
//System.out.println("Looking for method "+methnm);
m = null;
for(int c=0;c < methods.length;c++) {
Method m2 = methods[c];
//System.out.println("Checking method "+m2.getName());
if(m2.getName().equals(methnm)) {
//System.out.println("Found method "+m2.getName());
m = m2;
break;
}
}
m.invoke(builder, obj.get(key));
}
}
msg = ((Builder) builder).build();
} catch(ClassNotFoundException e0) {
throw new RuntimeException("Failed to decode message.", e0);
} catch(IllegalArgumentException e1) {
throw new RuntimeException("Failed to decode message.", e1);
} catch(IllegalAccessException e2) {
throw new RuntimeException("Failed to decode message.", e2);
} catch(InvocationTargetException e3) {
throw new RuntimeException("Failed to decode message.", e3);
} catch(SecurityException e4) {
throw new RuntimeException("Failed to decode message.", e4);
} catch(NoSuchMethodException e5) {
throw new RuntimeException("Failed to decode message.", e5);
}
return msg;
}
public static JSONObject encodeMessage(Message message) throws JSONException {
JSONObject obj = new JSONObject();
obj.put("_pb_class", message.getClass().getName());
Classextends Message> mclass = message.getClass();
Descriptor d = message.getDescriptorForType();
List fs = d.getFields();
for(FieldDescriptor f : fs) {
String key = f.getName();
String methnm = "get";
String[] parts = f.getName().split("_");
for(int i=0; i < parts.length; i++) {
String fc = parts[i].substring(0,1).toUpperCase();
parts[i] = parts[i].toLowerCase().substring(1,parts[i].length());
methnm += fc+parts[i];
}
Object result = null;
try {
Method m = mclass.getDeclaredMethod(methnm, (Class[])null);
result = m.invoke(message, (Object[])null);
} catch (Exception e) {
            throw new RuntimeException(e);
}
//System.out.println(">> FIELD: "+f.getName()+" "+f.getType()+" "+methnm+" "+result);
if (result == null) {
            obj.put(key, (String)null);
            } else if (result instanceof Message) {
            obj.put(key, encodeMessage((Message)result));
            } else if (result.getClass().isArray()) {
            throw new RuntimeException("Not implemented yet");
            //obj.put(key, new JSONArray(result, false));
            } else if (result instanceof Collection) { // List or Set
            throw new RuntimeException("Not implemented yet");
            //obj.put(key, new JSONArray((Collection)result, false));
            } else if (result instanceof Map) {
            throw new RuntimeException("Not implemented yet");
            //obj.put(key, new JSONObject((Map)result, includeSuperClass));
            } else if (isStandardProperty(result.getClass())) { // Primitives, String and Wrapper
            obj.put(key, result);
            } else {
                if (result.getClass().getPackage().getName().startsWith("java") ||
                        result.getClass().getClassLoader() == null) {
                obj.put(key, result.toString());
                } else { // User defined Objects
                throw new RuntimeException("Not implemented yet");
                    //obj.put(key, new JSONObject(result, includeSuperClass));
                }
            }
}
return obj;
}
}