protobuf: Support custom ProtobufCodec implementations
Some object types might not be annotated with @Column, but we may
still want to embed them into a protobuf format. Treat these as
opaque binary messages using the length delimited message format,
encoding them like any other nested message or arbitrary binary.
Change-Id: I8139e36838cbb2eabacbf3eab8c0156ddbdeb29b
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/protobuf/CodecGen.java b/src/main/java/com/google/gwtorm/protobuf/CodecGen.java
index 0ddc642..e8598ae 100644
--- a/src/main/java/com/google/gwtorm/protobuf/CodecGen.java
+++ b/src/main/java/com/google/gwtorm/protobuf/CodecGen.java
@@ -312,9 +312,20 @@
Class clazz = f.getNestedClass();
NestedCodec n = nestedCodecs.get(clazz);
if (n == null) {
- n = new NestedCodec("codec" + f.getColumnID(), //
- CodecFactory.encoder(clazz).getClass(), //
- Type.getType(clazz));
+ Class<? extends ProtobufCodec> codec = null;
+ Type type = Type.getType(clazz);
+ if (f.getField() != null) {
+ final CustomCodec cc = f.getField().getAnnotation(CustomCodec.class);
+ if (cc != null) {
+ codec = cc.value();
+ type = object;
+ }
+ }
+ if (codec == null) {
+ codec = CodecFactory.encoder(clazz).getClass();
+ }
+
+ n = new NestedCodec("codec" + f.getColumnID(), codec, type);
nestedCodecs.put(clazz, n);
}
return n;
@@ -390,7 +401,8 @@
private JavaColumnModel collectionColumn(final JavaColumnModel f,
final Class<?> valClazz) throws OrmException {
- return new JavaColumnModel(//
+ return new JavaColumnModel( //
+ f.getField(), //
f.getPathToFieldName(), //
f.getColumnID(), //
valClazz);
@@ -886,6 +898,10 @@
.getDescriptor());
mv.visitMethodInsn(INVOKEVIRTUAL, n.codecType.getInternalName(),
"newInstance", Type.getMethodDescriptor(n.pojoType, new Type[] {}));
+ if (object.equals(n.pojoType)) {
+ mv.visitTypeInsn(CHECKCAST, Type.getType(f.getNestedClass())
+ .getInternalName());
+ }
cgs.fieldSetEnd();
// read the length, set a new limit, decode the message, validate
diff --git a/src/main/java/com/google/gwtorm/protobuf/CustomCodec.java b/src/main/java/com/google/gwtorm/protobuf/CustomCodec.java
new file mode 100644
index 0000000..3462a3c
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/protobuf/CustomCodec.java
@@ -0,0 +1,34 @@
+// Copyright 2008 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 java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Identity of a custom {@link ProtobufCodec} for a {@code Column}.
+ * <p>
+ * Additional annotation tagged onto a {@code Column} field that carries the
+ * name of a custom {@link ProtobufCodec} that should be used to handle that
+ * field. The field data will be treated as an opaque binary sequence, so its
+ * {@link ProtobufCodec#sizeof(Object)} method must be accurate.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface CustomCodec {
+ Class<? extends ProtobufCodec<?>> value();
+}
diff --git a/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java b/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java
index 0627738..e024be3 100644
--- a/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java
+++ b/src/main/java/com/google/gwtorm/protobuf/ProtobufCodec.java
@@ -57,8 +57,12 @@
/** Encode the object into a byte array. */
public void encode(T obj, final byte[] data, int offset, int length) {
CodedOutputStream out = CodedOutputStream.newInstance(data, offset, length);
- encode(obj, out);
- flush(out);
+ try {
+ encode(obj, out);
+ out.flush();
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot encode message", err);
+ }
}
/** Encode the object into a ByteBuffer. */
@@ -68,22 +72,22 @@
buf.array(), //
buf.position(), //
buf.remaining());
- encode(obj, out);
- flush(out);
+ try {
+ encode(obj, out);
+ out.flush();
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot encode message", err);
+ }
buf.position(buf.position() + (buf.remaining() - out.spaceLeft()));
} else {
CodedOutputStream out = CodedOutputStream.newInstance(newStream(buf));
- encode(obj, out);
- flush(out);
- }
- }
-
- private static void flush(CodedOutputStream out) {
- try {
- out.flush();
- } catch (IOException err) {
- throw new RuntimeException("Cannot flush to in-memory buffer", err);
+ try {
+ encode(obj, out);
+ out.flush();
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot encode message", err);
+ }
}
}
@@ -100,8 +104,9 @@
*
* @param obj the object to encode.
* @param out the stream to encode the object onto.
+ * @throws IOException the underlying stream cannot be written to.
*/
- public abstract void encode(T obj, CodedOutputStream out);
+ public abstract void encode(T obj, CodedOutputStream out) throws IOException;
/** Compute the number of bytes of the encoded form of the object. */
public abstract int sizeof(T obj);
@@ -111,7 +116,11 @@
/** Decode a byte string into an object instance. */
public T decode(ByteString buf) {
- return decode(buf.newCodedInput());
+ try {
+ return decode(buf.newCodedInput());
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot decode message", err);
+ }
}
/** Decode a byte array into an object instance. */
@@ -121,7 +130,11 @@
/** Decode a byte array into an object instance. */
public T decode(byte[] data, int offset, int length) {
- return decode(CodedInputStream.newInstance(data, offset, length));
+ try {
+ return decode(CodedInputStream.newInstance(data, offset, length));
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot decode message", err);
+ }
}
/** Decode a byte buffer into an object instance. */
@@ -131,7 +144,12 @@
buf.array(), //
buf.position(), //
buf.remaining());
- T obj = decode(in);
+ T obj;
+ try {
+ obj = decode(in);
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot decode message", err);
+ }
buf.position(buf.position() + in.getTotalBytesRead());
return obj;
} else {
@@ -139,13 +157,21 @@
}
}
- /** Decode an object by reading it from the stream. */
- public T decode(CodedInputStream in) {
+ /**
+ * Decode an object by reading it from the stream.
+ *
+ * @throws IOException the underlying stream cannot be read.
+ */
+ public T decode(CodedInputStream in) throws IOException {
T obj = newInstance();
mergeFrom(in, obj);
return obj;
}
- /** Decode an input stream into an existing object instance. */
- public abstract void mergeFrom(CodedInputStream in, T obj);
+ /**
+ * Decode an input stream into an existing object instance.
+ *
+ * @throws IOException the underlying stream cannot be read.
+ */
+ public abstract void mergeFrom(CodedInputStream in, T obj) throws IOException;
}
diff --git a/src/main/java/com/google/gwtorm/schema/ColumnModel.java b/src/main/java/com/google/gwtorm/schema/ColumnModel.java
index df55a85..72bf017 100644
--- a/src/main/java/com/google/gwtorm/schema/ColumnModel.java
+++ b/src/main/java/com/google/gwtorm/schema/ColumnModel.java
@@ -57,11 +57,6 @@
protected void initNestedColumns(final Collection<? extends ColumnModel> col)
throws OrmException {
- if (col == null || col.isEmpty()) {
- throw new OrmException("Field " + getPathToFieldName()
- + " has no nested members inside type " + getNestedClassName());
- }
-
nestedColumns = new ArrayList<ColumnModel>(col);
recomputeColumnNames();
if (!isNotNull()) {
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 479b689..9aebf5b 100644
--- a/src/main/java/com/google/gwtorm/schema/java/JavaColumnModel.java
+++ b/src/main/java/com/google/gwtorm/schema/java/JavaColumnModel.java
@@ -29,11 +29,13 @@
public class JavaColumnModel extends ColumnModel {
+ private final Field field;
private final String fieldName;
private final Class<?> primitiveType;
private final Type genericType;
- public JavaColumnModel(final Field field) throws OrmException {
+ public JavaColumnModel(final Field f) throws OrmException {
+ field = f;
fieldName = field.getName();
primitiveType = field.getType();
genericType = field.getGenericType();
@@ -58,8 +60,9 @@
initNested();
}
- public JavaColumnModel(final String fieldPath, final int columnId,
+ public JavaColumnModel(Field f, final String fieldPath, final int columnId,
final Class<?> columnType) throws OrmException {
+ this.field = f;
this.fieldName = fieldPath;
this.columnName = fieldPath;
this.columnId = columnId;
@@ -117,6 +120,10 @@
return primitiveType;
}
+ public Field getField() {
+ return field;
+ }
+
private boolean isPrimitive() {
return Util.isSqlPrimitive(primitiveType);
}
diff --git a/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java b/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java
index 216f793..14544c0 100644
--- a/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java
+++ b/src/test/java/com/google/gwtorm/protobuf/ProtobufEncoderTest.java
@@ -17,9 +17,12 @@
import com.google.gwtorm.client.Column;
import com.google.gwtorm.data.TestAddress;
import com.google.gwtorm.data.TestPerson;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
import junit.framework.TestCase;
+import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -181,6 +184,19 @@
assertEquals(list.people, other.people);
}
+ public void testCustomEncoderList() {
+ ProtobufCodec<ItemList> e = CodecFactory.encoder(ItemList.class);
+
+ ItemList list = new ItemList();
+ list.list = new ArrayList<Item>();
+ list.list.add(new Item());
+ list.list.add(new Item());
+
+ ItemList other = e.decode(e.encodeToByteArray(list));
+ assertNotNull(other.list);
+ assertEquals(2, other.list.size());
+ }
+
private static String asString(byte[] bin)
throws UnsupportedEncodingException {
return new String(bin, "ISO-8859-1");
@@ -200,4 +216,35 @@
@Column(id = 1)
SortedSet<String> list;
}
+
+ static class Item {
+ }
+
+ static class ItemCodec extends ProtobufCodec<Item> {
+ @Override
+ public void encode(Item obj, CodedOutputStream out) throws IOException {
+ out.writeBoolNoTag(true);
+ }
+
+ @Override
+ public void mergeFrom(CodedInputStream in, Item obj) throws IOException {
+ in.readBool();
+ }
+
+ @Override
+ public Item newInstance() {
+ return new Item();
+ }
+
+ @Override
+ public int sizeof(Item obj) {
+ return 1;
+ }
+ }
+
+ static class ItemList {
+ @Column(id = 2)
+ @CustomCodec(ItemCodec.class)
+ List<Item> list;
+ }
}