Encode relation id as protobuf tag in content

Because we store all relations in effectively the same column, we
use a different protobuf field number wrapped around the individual
entity message.  This allows scanning programs that just examine
every row to be able to tell different relation's types apart and
correctly inspect the fields of each entity.

Change-Id: I26161a69ba78e97f0145db3d781ae60e96f66e0e
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/nosql/AccessGen.java b/src/main/java/com/google/gwtorm/nosql/AccessGen.java
index 7e01a5c..eb72b15 100644
--- a/src/main/java/com/google/gwtorm/nosql/AccessGen.java
+++ b/src/main/java/com/google/gwtorm/nosql/AccessGen.java
@@ -117,11 +117,17 @@
     return c;
   }
 
+  @SuppressWarnings("unchecked")
   private void initObjectCodec(final Class<?> clazz) throws OrmException {
+    ProtobufCodec oc = CodecFactory.encoder(modelClass);
+    if (model.getRelationID() > 0) {
+      oc = new RelationCodec(model.getRelationID(), oc);
+    }
+
     try {
       final Field e = clazz.getDeclaredField(F_OBJECT_CODEC);
       e.setAccessible(true);
-      e.set(null, CodecFactory.encoder(modelClass));
+      e.set(null, oc);
     } catch (IllegalArgumentException err) {
       throw new OrmException("Cannot setup ProtobufCodec", err);
     } catch (IllegalStateException err) {
diff --git a/src/main/java/com/google/gwtorm/nosql/RelationCodec.java b/src/main/java/com/google/gwtorm/nosql/RelationCodec.java
new file mode 100644
index 0000000..84add28
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/nosql/RelationCodec.java
@@ -0,0 +1,102 @@
+// Copyright 2010 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.nosql;
+
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.WireFormat;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+/** Encodes a relation number in front of an object. */
+public class RelationCodec<T> extends ProtobufCodec<T> {
+  /**
+   * Pop the field number from the stream and return it.
+   *
+   * @param in the stream to pop the field number from. The caller is
+   *        responsible for making sure the underlying stream had a mark set for
+   *        at least 8 bytes so the tag can be examined, reset, and later read
+   *        again during mergeFrom or decode.
+   * @return the field number of the relation.
+   * @throws IOException the stream cannot be read.
+   */
+  public static int peekId(CodedInputStream in) throws IOException {
+    return in.readTag() >>> 3;
+  }
+
+  private final int fieldId;
+  private final ProtobufCodec<T> objectCodec;
+
+  public RelationCodec(int fieldId, ProtobufCodec<T> objectCodec) {
+    this.fieldId = fieldId;
+    this.objectCodec = objectCodec;
+  }
+
+  @Override
+  public T newInstance() {
+    return objectCodec.newInstance();
+  }
+
+  @Override
+  public int sizeof(T obj) {
+    int sz = objectCodec.sizeof(obj);
+    return CodedOutputStream.computeTagSize(fieldId) //
+        + CodedOutputStream.computeRawVarint32Size(sz) //
+        + sz;
+  }
+
+  @Override
+  public void encode(T obj, CodedOutputStream out) throws IOException {
+    int sz = objectCodec.sizeof(obj);
+    out.writeTag(fieldId, WireFormat.FieldType.MESSAGE.getWireType());
+    out.writeRawVarint32(sz);
+    objectCodec.encode(obj, out);
+  }
+
+  @Override
+  public void mergeFrom(CodedInputStream in, T obj) throws IOException {
+    boolean found = false;
+    for (;;) {
+      int tag = in.readTag();
+      if (tag == 0) {
+        if (found) {
+          break;
+        } else {
+          // Reached EOF. But we require an object in our only field.
+          throw new EOFException("Expected field " + fieldId);
+        }
+      }
+
+      if ((tag >>> 3) == fieldId) {
+        if ((tag & 0x7) == WireFormat.FieldType.MESSAGE.getWireType()) {
+          int sz = in.readRawVarint32();
+          int oldLimit = in.pushLimit(sz);
+          objectCodec.mergeFrom(in, obj);
+          in.checkLastTagWas(0);
+          in.popLimit(oldLimit);
+          found = true;
+        } else {
+          throw new InvalidProtocolBufferException("Field " + fieldId
+              + " should be length delimited (wire type 2)");
+        }
+      } else {
+        in.skipField(tag);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/gwtorm/schema/RelationModel.java b/src/main/java/com/google/gwtorm/schema/RelationModel.java
index d835246..f8060ce 100644
--- a/src/main/java/com/google/gwtorm/schema/RelationModel.java
+++ b/src/main/java/com/google/gwtorm/schema/RelationModel.java
@@ -352,6 +352,7 @@
     r.append("Relation[\n");
     r.append("  method: " + getMethodName() + "\n");
     r.append("  table:  " + getRelationName() + "\n");
+    r.append("  id:     " + getRelationID() + "\n");
     r.append("  access: " + getAccessInterfaceName() + "\n");
     r.append("  entity: " + getEntityTypeClassName() + "\n");
     r.append("]");