Add support for encoding/decoding objects in Google protobuf

We avoid the full Google protobuf library, but instead setup a
simple encoder/decoder using the CodedInputStream and its matching
CodedOutputStream to handle message encoding and decoding directly
against the user object.

This allows us to serialize any object we support, without first
copying into the generated Message implementation that the protobuf
runtime would normally expect us to be using.

Testing is very limited.  We do however have a very simple mapping
of our objects onto protobuf's wire format.

Change-Id: Iec195ecacb1fb73458f27dc41d506379b5309f31
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/pom.xml b/pom.xml
index 63de7a6..f876c36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -330,6 +330,13 @@
       <version>5.1.10</version>
       <scope>test</scope>
     </dependency>
+
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+      <version>2.2.0</version>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
 
   <repositories>
diff --git a/src/main/java/com/google/gwtorm/jdbc/gen/GeneratedClassLoader.java b/src/main/java/com/google/gwtorm/jdbc/gen/GeneratedClassLoader.java
index 53643d3..d56d9e0 100644
--- a/src/main/java/com/google/gwtorm/jdbc/gen/GeneratedClassLoader.java
+++ b/src/main/java/com/google/gwtorm/jdbc/gen/GeneratedClassLoader.java
@@ -57,7 +57,8 @@
     super(parent);
   }
 
-  void defineClass(final String name, final byte[] code) throws OrmException {
+  public void defineClass(final String name, final byte[] code)
+      throws OrmException {
     if (debugCodeGen) {
       final File outClassFile =
           new File("generated_classes/" + name.replace('.', '/') + ".class");
diff --git a/src/main/java/com/google/gwtorm/protobuf/CodecFactory.java b/src/main/java/com/google/gwtorm/protobuf/CodecFactory.java
new file mode 100644
index 0000000..7db0f96
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/protobuf/CodecFactory.java
@@ -0,0 +1,82 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.protobuf;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.gen.GeneratedClassLoader;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/** Creates {@link ProtobufCodec} implementations on demand. */
+public final class CodecFactory {
+  private static final Map<Class<?>, String> encoders =
+      Collections.synchronizedMap(new WeakHashMap<Class<?>, String>());
+
+  /**
+   * Create an implementation to encode/decode an arbitrary object.
+   * <p>
+   * The object must use the {@link Column} annotations to denote the fields
+   * that should be encoded or decoded.
+   *
+   * @param <T> type of the object to be supported.
+   * @param type the object type.
+   * @return an encoder for this object type.
+   * @throws IllegalArgumentException the object's fields aren't declared
+   *         properly. This is a programming error that cannot be recovered.
+   */
+  public static <T> ProtobufCodec<T> encoder(Class<T> type)
+      throws IllegalStateException {
+    final GeneratedClassLoader loader = newLoader(type);
+    ProtobufCodec<T> encoder = null;
+    String cacheName = encoders.get(type);
+    if (cacheName != null) {
+      encoder = get(loader, cacheName);
+    }
+    if (encoder == null) {
+      final CodecGen<T> gen = new CodecGen<T>(loader, type);
+      try {
+        encoder = gen.create();
+      } catch (OrmException e) {
+        throw new IllegalArgumentException("Class " + type.getName()
+            + " cannot be supported on protobuf", e);
+      }
+      encoders.put(type, encoder.getClass().getName());
+    }
+    return encoder;
+  }
+
+  private static <T> GeneratedClassLoader newLoader(final Class<T> type) {
+    return new GeneratedClassLoader(type.getClassLoader());
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> ProtobufCodec<T> get(ClassLoader cl, String name) {
+    try {
+      return (ProtobufCodec<T>) Class.forName(name, true, cl).newInstance();
+    } catch (InstantiationException e) {
+      return null;
+    } catch (IllegalAccessException e) {
+      return null;
+    } catch (ClassNotFoundException e) {
+      return null;
+    }
+  }
+
+  private CodecFactory() {
+  }
+}
diff --git a/src/main/java/com/google/gwtorm/protobuf/CodecGen.java b/src/main/java/com/google/gwtorm/protobuf/CodecGen.java
new file mode 100644
index 0000000..6c79119
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/protobuf/CodecGen.java
@@ -0,0 +1,744 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.protobuf;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.gen.CodeGenSupport;
+import com.google.gwtorm.jdbc.gen.GeneratedClassLoader;
+import com.google.gwtorm.schema.ColumnModel;
+import com.google.gwtorm.schema.Util;
+import com.google.gwtorm.schema.java.JavaColumnModel;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.WireFormat;
+
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+
+/** Generates {@link ProtobufCodec} implementations. */
+class CodecGen<T> implements Opcodes {
+  private static final Type string = Type.getType(String.class);
+  private static final Type byteStringOutput =
+      Type.getType(ByteString.Output.class);
+  private static final Type byteString = Type.getType(ByteString.class);
+  private static final Type object = Type.getType(Object.class);
+  private static final Type codedOutputStream =
+      Type.getType(CodedOutputStream.class);
+  private static final Type codedInputStream =
+      Type.getType(CodedInputStream.class);
+  private final GeneratedClassLoader classLoader;
+  private final Class<T> pojo;
+  private final Type pojoType;
+
+  private ClassWriter cw;
+  private JavaColumnModel[] myFields;
+  private String superTypeName;
+  private String implClassName;
+  private String implTypeName;
+
+  public CodecGen(final GeneratedClassLoader loader, final Class<T> t) {
+    classLoader = loader;
+    pojo = t;
+    pojoType = Type.getType(pojo);
+  }
+
+  public ProtobufCodec<T> create() throws OrmException {
+    myFields = scanFields(pojo);
+
+    init();
+    implementConstructor();
+    implementSizeof();
+    implementEncode();
+    implementDecode();
+    cw.visitEnd();
+    classLoader.defineClass(implClassName, cw.toByteArray());
+
+    try {
+      final Class<?> c = Class.forName(implClassName, true, classLoader);
+      return cast(c.newInstance());
+    } catch (InstantiationException e) {
+      throw new OrmException("Cannot create new encoder", e);
+    } catch (IllegalAccessException e) {
+      throw new OrmException("Cannot create new encoder", e);
+    } catch (ClassNotFoundException e) {
+      throw new OrmException("Cannot create new encoder", e);
+    }
+  }
+
+  private static JavaColumnModel[] scanFields(Class<?> in) throws OrmException {
+    final Collection<JavaColumnModel> col = new ArrayList<JavaColumnModel>();
+    while (in != null) {
+      for (final Field f : in.getDeclaredFields()) {
+        if (f.getAnnotation(Column.class) != null) {
+          col.add(new JavaColumnModel(f));
+        }
+      }
+      in = in.getSuperclass();
+    }
+    return sort(col);
+  }
+
+  private static JavaColumnModel[] sort(
+      final Collection<? extends ColumnModel> col) {
+    JavaColumnModel[] out = col.toArray(new JavaColumnModel[col.size()]);
+    Arrays.sort(out, new Comparator<JavaColumnModel>() {
+      @Override
+      public int compare(JavaColumnModel o1, JavaColumnModel o2) {
+        return o1.getColumnID() - o2.getColumnID();
+      }
+    });
+    return out;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> ProtobufCodec<T> cast(final Object c) {
+    return (ProtobufCodec<T>) c;
+  }
+
+  private void init() {
+    superTypeName = Type.getInternalName(ProtobufCodec.class);
+    implClassName = pojo.getName() + "_protobuf_" + Util.createRandomName();
+    implTypeName = implClassName.replace('.', '/');
+
+    cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
+    cw.visit(V1_3, ACC_PUBLIC | ACC_FINAL | ACC_SUPER, implTypeName, null,
+        superTypeName, new String[] {});
+  }
+
+  private void implementConstructor() {
+    final String consName = "<init>";
+    final String consDesc =
+        Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {});
+    final MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC, consName, consDesc, null, null);
+    mv.visitCode();
+
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitMethodInsn(INVOKESPECIAL, superTypeName, consName, consDesc);
+
+    mv.visitInsn(RETURN);
+    mv.visitMaxs(-1, -1);
+    mv.visitEnd();
+  }
+
+  private void implementSizeof() throws OrmException {
+    final MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC, "sizeof", Type.getMethodDescriptor(
+            Type.INT_TYPE, new Type[] {object}), null, new String[] {});
+    mv.visitCode();
+    final SizeofCGS cgs = new SizeofCGS(mv);
+    cgs.setEntityType(pojoType);
+
+    mv.visitVarInsn(ALOAD, 1);
+    mv.visitTypeInsn(CHECKCAST, pojoType.getInternalName());
+    mv.visitVarInsn(ASTORE, 1);
+
+    cgs.push(0);
+    mv.visitVarInsn(ISTORE, cgs.sizeVar);
+    sizeofMessage(myFields, mv, cgs);
+
+    mv.visitVarInsn(ILOAD, cgs.sizeVar);
+    mv.visitInsn(IRETURN);
+    mv.visitMaxs(-1, -1);
+    mv.visitEnd();
+  }
+
+  private static void sizeofMessage(final JavaColumnModel[] myFields,
+      final MethodVisitor mv, final SizeofCGS cgs) throws OrmException {
+    for (final JavaColumnModel f : myFields) {
+      if (f.isNested()) {
+        final Label end = new Label();
+        cgs.setFieldReference(f);
+        cgs.pushFieldValue();
+        mv.visitJumpInsn(IFNULL, end);
+
+        final int oldVar = cgs.sizeVar;
+        final int msgVar = cgs.newLocal();
+        cgs.sizeVar = msgVar;
+        cgs.push(0);
+        mv.visitVarInsn(ISTORE, cgs.sizeVar);
+
+        sizeofMessage(sort(f.getNestedColumns()), mv, cgs);
+        cgs.sizeVar = oldVar;
+
+        cgs.push(f.getColumnID());
+        cgs.inc("computeTagSize", Type.INT_TYPE);
+
+        mv.visitVarInsn(ILOAD, msgVar);
+        cgs.inc("computeRawVarint32Size", Type.INT_TYPE);
+
+        mv.visitVarInsn(ILOAD, msgVar);
+        cgs.inc();
+
+        cgs.freeLocal(msgVar);
+        mv.visitLabel(end);
+      } else {
+        sizeofScalar(mv, cgs, f);
+      }
+    }
+  }
+
+  private static void sizeofScalar(final MethodVisitor mv, final SizeofCGS cgs,
+      final JavaColumnModel f) throws OrmException {
+    cgs.setFieldReference(f);
+
+    switch (Type.getType(f.getPrimitiveType()).getSort()) {
+      case Type.BOOLEAN:
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.inc("computeBoolSize", Type.INT_TYPE, Type.BOOLEAN_TYPE);
+        break;
+
+      case Type.CHAR:
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.inc("computeUInt32Size", Type.INT_TYPE, Type.INT_TYPE);
+        break;
+
+      case Type.BYTE:
+      case Type.SHORT:
+      case Type.INT:
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.inc("computeSInt32Size", Type.INT_TYPE, Type.INT_TYPE);
+        break;
+
+      case Type.FLOAT:
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.inc("computeFloatSize", Type.INT_TYPE, Type.FLOAT_TYPE);
+        break;
+
+      case Type.DOUBLE:
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.inc("computeDoubleSize", Type.INT_TYPE, Type.DOUBLE_TYPE);
+        break;
+
+      case Type.LONG:
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.inc("computeSInt64", Type.INT_TYPE, Type.LONG_TYPE);
+        break;
+
+      case Type.ARRAY:
+      case Type.OBJECT: {
+        final Label end = new Label();
+        cgs.pushFieldValue();
+        mv.visitJumpInsn(IFNULL, end);
+
+        if (f.getPrimitiveType() == byte[].class) {
+          cgs.push(f.getColumnID());
+          cgs.inc("computeTagSize", Type.INT_TYPE);
+
+          cgs.pushFieldValue();
+          mv.visitInsn(ARRAYLENGTH);
+          cgs.inc("computeRawVarint32Size", Type.INT_TYPE);
+
+          cgs.pushFieldValue();
+          mv.visitInsn(ARRAYLENGTH);
+          cgs.inc();
+
+        } else if (f.getPrimitiveType() == String.class) {
+          cgs.push(f.getColumnID());
+          cgs.pushFieldValue();
+          cgs.inc("computeStringSize", Type.INT_TYPE, string);
+
+        } else if (f.getPrimitiveType() == java.sql.Timestamp.class
+            || f.getPrimitiveType() == java.util.Date.class
+            || f.getPrimitiveType() == java.sql.Date.class) {
+          cgs.push(f.getColumnID());
+          String tsType = Type.getType(f.getPrimitiveType()).getInternalName();
+          mv.visitMethodInsn(INVOKEVIRTUAL, tsType, "getTime", Type
+              .getMethodDescriptor(Type.LONG_TYPE, new Type[] {}));
+          cgs.inc("computeFixed64Size", Type.INT_TYPE, Type.LONG_TYPE);
+
+        } else {
+          throw new OrmException("Type " + f.getPrimitiveType()
+              + " not supported for field " + f.getPathToFieldName());
+        }
+        mv.visitLabel(end);
+        break;
+      }
+
+      default:
+        throw new OrmException("Type " + f.getPrimitiveType()
+            + " not supported for field " + f.getPathToFieldName());
+    }
+  }
+
+  private void implementEncode() throws OrmException {
+    final MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC, "encode", Type.getMethodDescriptor(
+            byteString, new Type[] {object}), null, new String[] {});
+    mv.visitCode();
+    final EncodeCGS cgs = new EncodeCGS(mv);
+    cgs.setEntityType(pojoType);
+
+    mv.visitVarInsn(ALOAD, 1);
+    mv.visitTypeInsn(CHECKCAST, pojoType.getInternalName());
+    mv.visitVarInsn(ASTORE, 1);
+
+    encodeMessage(myFields, mv, cgs);
+
+    mv.visitInsn(ARETURN);
+    mv.visitMaxs(-1, -1);
+    mv.visitEnd();
+  }
+
+  private static void encodeMessage(final JavaColumnModel[] myFields,
+      final MethodVisitor mv, final EncodeCGS cgs) throws OrmException {
+    final int oldVar = cgs.codedOutputStreamVar;
+    cgs.codedOutputStreamVar = cgs.newLocal();
+
+    final int strVar = cgs.newLocal();
+    mv.visitMethodInsn(INVOKESTATIC, byteString.getInternalName(), "newOutput",
+        Type.getMethodDescriptor(byteStringOutput, new Type[] {}));
+    mv.visitVarInsn(ASTORE, strVar);
+
+    mv.visitVarInsn(ALOAD, strVar);
+    mv.visitMethodInsn(INVOKESTATIC, codedOutputStream.getInternalName(),
+        "newInstance", Type.getMethodDescriptor(codedOutputStream,
+            new Type[] {Type.getType(OutputStream.class)}));
+    mv.visitVarInsn(ASTORE, cgs.codedOutputStreamVar);
+
+    for (final JavaColumnModel f : myFields) {
+      if (f.isNested()) {
+        final Label end = new Label();
+        cgs.setFieldReference(f);
+        cgs.pushFieldValue();
+        mv.visitJumpInsn(IFNULL, end);
+
+        final int v = cgs.newLocal();
+        encodeMessage(sort(f.getNestedColumns()), mv, cgs);
+        mv.visitVarInsn(ASTORE, v);
+
+        mv.visitVarInsn(ALOAD, v);
+        mv.visitMethodInsn(INVOKEVIRTUAL, byteString.getInternalName(), "size",
+            Type.getMethodDescriptor(Type.INT_TYPE, new Type[] {}));
+        mv.visitJumpInsn(IFEQ, end);
+
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        mv.visitVarInsn(ALOAD, v);
+        cgs.write("writeBytes", byteString);
+
+        cgs.freeLocal(v);
+        mv.visitLabel(end);
+      } else {
+        encodeScalar(mv, cgs, f);
+      }
+    }
+
+    cgs.pushCodedOutputStream();
+    mv.visitMethodInsn(INVOKEVIRTUAL, codedOutputStream.getInternalName(),
+        "flush", Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
+
+    cgs.freeLocal(cgs.codedOutputStreamVar);
+    cgs.codedOutputStreamVar = oldVar;
+
+    mv.visitVarInsn(ALOAD, strVar);
+    mv.visitMethodInsn(INVOKEVIRTUAL, byteStringOutput.getInternalName(),
+        "toByteString", Type.getMethodDescriptor(byteString, new Type[] {}));
+    cgs.freeLocal(strVar);
+  }
+
+  private static void encodeScalar(final MethodVisitor mv, final EncodeCGS cgs,
+      final JavaColumnModel f) throws OrmException {
+    cgs.setFieldReference(f);
+
+    switch (Type.getType(f.getPrimitiveType()).getSort()) {
+      case Type.BOOLEAN:
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.write("writeBool", Type.BOOLEAN_TYPE);
+        break;
+
+      case Type.CHAR:
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.write("writeUInt32", Type.INT_TYPE);
+        break;
+
+      case Type.BYTE:
+      case Type.SHORT:
+      case Type.INT:
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.write("writeSInt32", Type.INT_TYPE);
+        break;
+
+      case Type.FLOAT:
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.write("writeFloat", Type.FLOAT_TYPE);
+        break;
+
+      case Type.DOUBLE:
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.write("writeDouble", Type.DOUBLE_TYPE);
+        break;
+
+      case Type.LONG:
+        cgs.pushCodedOutputStream();
+        cgs.push(f.getColumnID());
+        cgs.pushFieldValue();
+        cgs.write("writeSInt64", Type.LONG_TYPE);
+        break;
+
+      case Type.ARRAY:
+      case Type.OBJECT: {
+        final Label end = new Label();
+        cgs.pushFieldValue();
+        mv.visitJumpInsn(IFNULL, end);
+
+        if (f.getPrimitiveType() == byte[].class) {
+          cgs.pushCodedOutputStream();
+          cgs.push(f.getColumnID());
+          cgs.push(WireFormat.FieldType.BYTES.getWireType());
+          mv.visitMethodInsn(INVOKEVIRTUAL,
+              codedOutputStream.getInternalName(), "writeTag", Type
+                  .getMethodDescriptor(Type.VOID_TYPE, new Type[] {
+                      Type.INT_TYPE, Type.INT_TYPE}));
+
+          cgs.pushCodedOutputStream();
+          cgs.pushFieldValue();
+          mv.visitInsn(ARRAYLENGTH);
+          mv.visitMethodInsn(INVOKEVIRTUAL,
+              codedOutputStream.getInternalName(), "writeRawVarint32", Type
+                  .getMethodDescriptor(Type.VOID_TYPE,
+                      new Type[] {Type.INT_TYPE}));
+
+          cgs.pushCodedOutputStream();
+          cgs.pushFieldValue();
+          mv.visitMethodInsn(INVOKEVIRTUAL,
+              codedOutputStream.getInternalName(), "writeRawBytes", Type
+                  .getMethodDescriptor(Type.VOID_TYPE, new Type[] {Type
+                      .getType(byte[].class)}));
+
+        } else {
+          cgs.pushCodedOutputStream();
+          cgs.push(f.getColumnID());
+          cgs.pushFieldValue();
+
+          if (f.getPrimitiveType() == String.class) {
+            cgs.write("writeString", string);
+
+          } else if (f.getPrimitiveType() == java.sql.Timestamp.class
+              || f.getPrimitiveType() == java.util.Date.class
+              || f.getPrimitiveType() == java.sql.Date.class) {
+            String tsType =
+                Type.getType(f.getPrimitiveType()).getInternalName();
+            mv.visitMethodInsn(INVOKEVIRTUAL, tsType, "getTime", Type
+                .getMethodDescriptor(Type.LONG_TYPE, new Type[] {}));
+            cgs.write("writeFixed64", Type.LONG_TYPE);
+
+          } else {
+            throw new OrmException("Type " + f.getPrimitiveType()
+                + " not supported for field " + f.getPathToFieldName());
+          }
+        }
+        mv.visitLabel(end);
+        break;
+      }
+
+      default:
+        throw new OrmException("Type " + f.getPrimitiveType()
+            + " not supported for field " + f.getPathToFieldName());
+    }
+  }
+
+  private void implementDecode() throws OrmException {
+    final Type retType = object;
+    final MethodVisitor mv =
+        cw.visitMethod(ACC_PROTECTED, "decode", Type.getMethodDescriptor(
+            retType, new Type[] {codedInputStream}), null, new String[] {});
+    mv.visitCode();
+    final DecodeCGS cgs = new DecodeCGS(mv);
+
+    cgs.setEntityType(pojoType);
+
+    mv.visitTypeInsn(NEW, pojoType.getInternalName());
+    mv.visitInsn(DUP);
+    mv.visitMethodInsn(INVOKESPECIAL, pojoType.getInternalName(), "<init>",
+        Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
+    mv.visitVarInsn(ASTORE, cgs.objVar);
+
+    final int tagVar = cgs.newLocal();
+    decodeMessage(myFields, mv, cgs);
+
+    cgs.pushEntity();
+    mv.visitInsn(ARETURN);
+    mv.visitMaxs(-1, -1);
+    mv.visitEnd();
+  }
+
+  private static void decodeMessage(final JavaColumnModel[] myFields,
+      final MethodVisitor mv, final DecodeCGS cgs) throws OrmException {
+    final Label nextField = new Label();
+    final Label end = new Label();
+    mv.visitLabel(nextField);
+
+    // while (!ci.isAtEnd) { ...
+    cgs.call("readTag", Type.INT_TYPE);
+    mv.visitInsn(DUP);
+    mv.visitVarInsn(ISTORE, cgs.tagVar);
+
+    cgs.push(3);
+    mv.visitInsn(IUSHR);
+
+    final Label badField = new Label();
+    final int[] caseTags = new int[1 + myFields.length];
+    final Label[] caseLabels = new Label[caseTags.length];
+
+    caseTags[0] = 0;
+    caseLabels[0] = new Label();
+
+    int gaps = 0;
+    for (int i = 1; i < caseTags.length; i++) {
+      caseTags[i] = myFields[i - 1].getColumnID();
+      caseLabels[i] = new Label();
+      gaps += caseTags[i] - (caseTags[i - 1] + 1);
+    }
+
+    if (2 * gaps / 3 <= myFields.length) {
+      final int min = 0;
+      final int max = caseTags[caseTags.length - 1];
+      final Label[] table = new Label[max + 1];
+      Arrays.fill(table, badField);
+      for (int idx = 0; idx < caseTags.length; idx++) {
+        table[caseTags[idx]] = caseLabels[idx];
+      }
+      mv.visitTableSwitchInsn(min, max, badField, table);
+    } else {
+      mv.visitLookupSwitchInsn(badField, caseTags, caseLabels);
+    }
+
+    mv.visitLabel(caseLabels[0]);
+    mv.visitJumpInsn(GOTO, end);
+
+    for (int idx = 1; idx < caseTags.length; idx++) {
+      final JavaColumnModel f = myFields[idx - 1];
+      mv.visitLabel(caseLabels[idx]);
+      if (f.isNested()) {
+        final Label load = new Label();
+        cgs.setFieldReference(f);
+        cgs.pushFieldValue();
+        mv.visitJumpInsn(IFNONNULL, load);
+        cgs.fieldSetBegin();
+        mv.visitTypeInsn(NEW, Type.getType(f.getNestedClass())
+            .getInternalName());
+        mv.visitInsn(DUP);
+        mv.visitMethodInsn(INVOKESPECIAL, Type.getType(f.getNestedClass())
+            .getInternalName(), "<init>", Type.getMethodDescriptor(
+            Type.VOID_TYPE, new Type[] {}));
+        cgs.fieldSetEnd();
+
+        // read the length, set a new limit, decode the message, validate
+        // we stopped at the end of it as expected.
+        //
+        mv.visitLabel(load);
+        final int limitVar = cgs.newLocal();
+        cgs.pushCodedInputStream();
+        cgs.call("readRawVarint32", Type.INT_TYPE);
+        cgs.ncallInt("pushLimit", Type.INT_TYPE);
+        mv.visitVarInsn(ISTORE, limitVar);
+
+        decodeMessage(sort(f.getNestedColumns()), mv, cgs);
+
+        cgs.pushCodedInputStream();
+        mv.visitVarInsn(ILOAD, limitVar);
+        cgs.ncallInt("popLimit", Type.VOID_TYPE);
+        cgs.freeLocal(limitVar);
+
+      } else {
+        decodeScalar(mv, cgs, f);
+      }
+      mv.visitJumpInsn(GOTO, nextField);
+    }
+
+    // default:
+    mv.visitLabel(badField);
+    cgs.pushCodedInputStream();
+    mv.visitVarInsn(ILOAD, cgs.tagVar);
+    cgs.ncallInt("skipField", Type.BOOLEAN_TYPE);
+    mv.visitInsn(POP);
+    mv.visitJumpInsn(GOTO, nextField);
+
+    mv.visitLabel(end);
+    cgs.pushCodedInputStream();
+    cgs.push(0);
+    cgs.ncallInt("checkLastTagWas", Type.VOID_TYPE);
+  }
+
+  private static void decodeScalar(final MethodVisitor mv, final DecodeCGS cgs,
+      final JavaColumnModel f) throws OrmException {
+    cgs.setFieldReference(f);
+    cgs.fieldSetBegin();
+    switch (Type.getType(f.getPrimitiveType()).getSort()) {
+      case Type.BOOLEAN:
+        cgs.call("readBool", Type.BOOLEAN_TYPE);
+        break;
+
+      case Type.CHAR:
+        cgs.call("readUInt32", Type.INT_TYPE);
+        break;
+
+      case Type.BYTE:
+      case Type.SHORT:
+      case Type.INT:
+        cgs.call("readSInt32", Type.INT_TYPE);
+        break;
+
+      case Type.FLOAT:
+        cgs.call("readFloat", Type.FLOAT_TYPE);
+        break;
+
+      case Type.DOUBLE:
+        cgs.call("readDouble", Type.DOUBLE_TYPE);
+        break;
+
+      case Type.LONG:
+        cgs.call("readSInt64", Type.LONG_TYPE);
+        break;
+
+      default:
+        if (f.getPrimitiveType() == byte[].class) {
+          cgs.call("readBytes", byteString);
+          mv.visitMethodInsn(INVOKEVIRTUAL, byteString.getInternalName(),
+              "toByteArray", Type.getMethodDescriptor(Type
+                  .getType(byte[].class), new Type[] {}));
+
+        } else if (f.getPrimitiveType() == String.class) {
+          cgs.call("readString", string);
+
+        } else if (f.getPrimitiveType() == java.sql.Timestamp.class
+            || f.getPrimitiveType() == java.util.Date.class
+            || f.getPrimitiveType() == java.sql.Date.class) {
+          String tsType = Type.getType(f.getPrimitiveType()).getInternalName();
+          mv.visitTypeInsn(NEW, tsType);
+          mv.visitInsn(DUP);
+          cgs.call("readFixed64", Type.LONG_TYPE);
+          mv.visitMethodInsn(INVOKESPECIAL, tsType, "<init>", Type
+              .getMethodDescriptor(Type.VOID_TYPE, new Type[] {}));
+
+        } else {
+          throw new OrmException("Type " + f.getPrimitiveType()
+              + " not supported for field " + f.getPathToFieldName());
+        }
+        break;
+    }
+    cgs.fieldSetEnd();
+  }
+
+  private static final class SizeofCGS extends CodeGenSupport {
+    int sizeVar;
+
+    private SizeofCGS(MethodVisitor method) {
+      super(method);
+      sizeVar = newLocal();
+    }
+
+    void inc(String name, Type... args) {
+      mv.visitMethodInsn(INVOKESTATIC, codedOutputStream.getInternalName(),
+          name, Type.getMethodDescriptor(Type.INT_TYPE, args));
+      inc();
+    }
+
+    void inc() {
+      mv.visitVarInsn(ILOAD, sizeVar);
+      mv.visitInsn(IADD);
+      mv.visitVarInsn(ISTORE, sizeVar);
+    }
+
+    @Override
+    public void pushEntity() {
+      mv.visitVarInsn(ALOAD, 1);
+    }
+  }
+
+  private static final class EncodeCGS extends CodeGenSupport {
+    int codedOutputStreamVar;
+
+    private EncodeCGS(MethodVisitor method) {
+      super(method);
+    }
+
+    void pushCodedOutputStream() {
+      mv.visitVarInsn(ALOAD, codedOutputStreamVar);
+    }
+
+    void write(String name, Type arg) {
+      mv.visitMethodInsn(INVOKEVIRTUAL, codedOutputStream.getInternalName(),
+          name, Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {
+              Type.INT_TYPE, arg}));
+    }
+
+    @Override
+    public void pushEntity() {
+      mv.visitVarInsn(ALOAD, 1);
+    }
+  }
+
+  private static final class DecodeCGS extends CodeGenSupport {
+    final int codedInputStreamVar = 1;
+    final int objVar;
+    final int tagVar;
+
+    private DecodeCGS(MethodVisitor method) {
+      super(method);
+      objVar = newLocal();
+      tagVar = newLocal();
+    }
+
+    void pushCodedInputStream() {
+      mv.visitVarInsn(ALOAD, codedInputStreamVar);
+    }
+
+    void call(String name, Type ret) {
+      pushCodedInputStream();
+      mv.visitMethodInsn(INVOKEVIRTUAL, codedInputStream.getInternalName(),
+          name, Type.getMethodDescriptor(ret, new Type[] {}));
+    }
+
+    void ncallInt(String name, Type ret) {
+      mv.visitMethodInsn(INVOKEVIRTUAL, codedInputStream.getInternalName(),
+          name, Type.getMethodDescriptor(ret, new Type[] {Type.INT_TYPE}));
+    }
+
+    @Override
+    public void pushEntity() {
+      mv.visitVarInsn(ALOAD, objVar);
+    }
+  }
+}
diff --git a/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java b/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java
new file mode 100644
index 0000000..d13e7b3
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java
@@ -0,0 +1,46 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.protobuf;
+
+import com.google.gwtorm.client.Column;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedInputStream;
+
+/**
+ * Encode and decode an arbitrary Java object as a Protobuf message.
+ * <p>
+ * The object must use the {@link Column} annotations to denote the fields that
+ * should be encoded or decoded.
+ */
+public abstract class ProtobufCodec<T> {
+  /** Encode the object into an immutable byte string. */
+  public abstract ByteString encode(T obj);
+
+  /** Compute the number of bytes of the encoded form of the object. */
+  public abstract int sizeof(T obj);
+
+  /** Decode a byte string into an object instance. */
+  public T decode(ByteString buf) {
+    return decode(buf.newCodedInput());
+  }
+
+  /** Decode a byte string into an object instance. */
+  public T decode(byte[] buf) {
+    return decode(CodedInputStream.newInstance(buf));
+  }
+
+  /** Decode an object by reading it from the stream. */
+  protected abstract T decode(CodedInputStream in);
+}
diff --git a/src/main/java/com/google/gwtorm/schema/java/JavaColumnModel.java b/src/main/java/com/google/gwtorm/schema/java/JavaColumnModel.java
index 082a6a2..984da77 100644
--- a/src/main/java/com/google/gwtorm/schema/java/JavaColumnModel.java
+++ b/src/main/java/com/google/gwtorm/schema/java/JavaColumnModel.java
@@ -26,10 +26,10 @@
 import java.util.List;
 
 
-class JavaColumnModel extends ColumnModel {
+public class JavaColumnModel extends ColumnModel {
   private final Field field;
 
-  JavaColumnModel(final Field columnField) throws OrmException {
+  public JavaColumnModel(final Field columnField) throws OrmException {
     field = columnField;
     initName(field.getName(), field.getAnnotation(Column.class));
 
@@ -78,6 +78,10 @@
     return isPrimitive() ? null : field.getType().getName();
   }
 
+  public Class<?> getNestedClass() {
+    return field.getType();
+  }
+
   private boolean isPrimitive() {
     return Util.isSqlPrimitive(field.getType());
   }
diff --git a/src/test/java/com/google/gwtorm/data/TestAddress.java b/src/test/java/com/google/gwtorm/data/TestAddress.java
index f3df900..d5e330f 100644
--- a/src/test/java/com/google/gwtorm/data/TestAddress.java
+++ b/src/test/java/com/google/gwtorm/data/TestAddress.java
@@ -74,4 +74,8 @@
   public String location() {
     return location;
   }
+
+  public TestAddress.Key key() {
+    return city;
+  }
 }
diff --git a/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java b/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java
new file mode 100644
index 0000000..15fc754
--- /dev/null
+++ b/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java
@@ -0,0 +1,70 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.protobuf;
+
+import com.google.gwtorm.data.TestAddress;
+import com.google.gwtorm.data.TestPerson;
+
+import junit.framework.TestCase;
+
+import java.io.UnsupportedEncodingException;
+
+public class ProtobufEncoderTest extends TestCase {
+  @SuppressWarnings("cast")
+  public void testPerson() throws UnsupportedEncodingException {
+    final ProtobufCodec<TestPerson> e = CodecFactory.encoder(TestPerson.class);
+    final byte[] bin = new byte[] {
+    //
+        // name
+        0x0a, 0x09,
+        // name.name
+        0x0a, 0x07, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, //
+        // age
+        0x10, (byte) 0x96, 0x01, //
+        // registered (true)
+        0x18, 0x01 //
+        //
+        };
+    TestPerson p = e.decode(bin);
+    assertNotNull(p);
+    assertTrue(p instanceof TestPerson);
+    assertEquals("testing", p.name());
+    assertEquals(75, p.age());
+    assertTrue(p.isRegistered());
+
+    final byte[] out = e.encode(p).toByteArray();
+    assertEquals(new String(bin, "ISO-8859-1"), new String(out, "ISO-8859-1"));
+    assertEquals(bin.length, e.sizeof(p));
+  }
+
+  public void testAddress() {
+    final ProtobufCodec<TestAddress> e =
+        CodecFactory.encoder(TestAddress.class);
+    TestAddress a = e.decode(new byte[0]);
+    assertNotNull(a);
+    assertNull(a.location());
+
+    TestPerson.Key k = new TestPerson.Key("bob");
+    TestPerson p = new TestPerson(k, 42);
+    TestAddress b = new TestAddress(new TestAddress.Key(k, "ny"), "ny");
+
+    byte[] act = e.encode(b).toByteArray();
+
+    TestAddress c = e.decode(act);
+    assertEquals(c.location(), b.location());
+    assertEquals(c.city(), b.city());
+    assertEquals(c.key(), b.key());
+  }
+}