Generate a Protobuf definition for the database

I added support for generating a file containing all the Protobuf
type definitions for a given database schema. The JavaSchemaModel now
has a method to output a Protobuf definition to a given PrintWriter.
This allows tools using a NoSQL database to have a Protobuf file
describing the Protobuf data in the database.

Change-Id: I53f4902d2b335be2b20d966fd0eb58c050e8a53c
diff --git a/src/main/java/com/google/gwtorm/schema/RelationModel.java b/src/main/java/com/google/gwtorm/schema/RelationModel.java
index de323f8..d835246 100644
--- a/src/main/java/com/google/gwtorm/schema/RelationModel.java
+++ b/src/main/java/com/google/gwtorm/schema/RelationModel.java
@@ -123,6 +123,10 @@
     return relationName;
   }
 
+  public int getRelationID() {
+    return relation.id();
+  }
+
   public Collection<ColumnModel> getDependentFields() {
     final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
     for (final ColumnModel c : fieldsByFieldName.values()) {
diff --git a/src/main/java/com/google/gwtorm/schema/java/JavaSchemaModel.java b/src/main/java/com/google/gwtorm/schema/java/JavaSchemaModel.java
index b8a0748..390ef3e 100644
--- a/src/main/java/com/google/gwtorm/schema/java/JavaSchemaModel.java
+++ b/src/main/java/com/google/gwtorm/schema/java/JavaSchemaModel.java
@@ -22,6 +22,7 @@
 import com.google.gwtorm.schema.SchemaModel;
 import com.google.gwtorm.schema.SequenceModel;
 
+import java.io.PrintWriter;
 import java.lang.reflect.Method;
 
 
@@ -65,6 +66,11 @@
     throw new IllegalArgumentException("No relation named " + name);
   }
 
+  public void generateProto(PrintWriter out) {
+    ProtoFileGenerator pfg = new ProtoFileGenerator(schema.getSimpleName(), getRelations());
+    pfg.print(out);
+  }
+
   @Override
   public String getSchemaClassName() {
     return schema.getName();
diff --git a/src/main/java/com/google/gwtorm/schema/java/ProtoFileGenerator.java b/src/main/java/com/google/gwtorm/schema/java/ProtoFileGenerator.java
new file mode 100644
index 0000000..66f617e
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/schema/java/ProtoFileGenerator.java
@@ -0,0 +1,209 @@
+// 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.schema.java;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.schema.ColumnModel;
+import com.google.gwtorm.schema.RelationModel;
+
+import org.objectweb.asm.Type;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+
+class ProtoFileGenerator {
+  private static final Comparator<ColumnModel> COLUMN_COMPARATOR =
+      new Comparator<ColumnModel>() {
+        @Override
+        public int compare(ColumnModel o1, ColumnModel o2) {
+          return o1.getColumnID() - o2.getColumnID();
+        }
+      };
+
+  private static final Comparator<RelationModel> RELATION_COMPARATOR =
+      new Comparator<RelationModel>() {
+        @Override
+        public int compare(RelationModel o1, RelationModel o2) {
+          return o1.getRelationID() - o2.getRelationID();
+        }
+      };
+
+  private final Collection<RelationModel> rels;
+  private final String schemaName;
+  private final HashSet<String> seen;
+  private final HashSet<String> collisions;
+
+  ProtoFileGenerator(String schemaName, Collection<RelationModel> relations) {
+    this.schemaName = schemaName;
+    this.rels = relations;
+    this.seen = new HashSet<String>();
+    this.collisions = new HashSet<String>();
+  }
+
+  void print(PrintWriter out) {
+    seen.clear();
+    collisions.clear();
+
+    for (RelationModel r : rels) {
+      for (ColumnModel c : r.getColumns()) {
+        if (c.isNested()) {
+          String type = getShortClassName(c);
+          if (seen.contains(type)) {
+            collisions.add(type);
+          } else {
+            seen.add(type);
+          }
+        }
+      }
+    }
+
+    seen.clear();
+    for (RelationModel r : rels) {
+      generateMessage(r, out);
+    }
+
+    out.print("message " + schemaName + " {\n");
+
+    for (RelationModel r : sortRelations(rels)) {
+      out.print("\toptional " + getMessageName(r) + " "
+          + r.getRelationName().toLowerCase() + " = " + r.getRelationID()
+          + ";\n");
+    }
+
+    out.print("}\n");
+  }
+
+  private void generateMessage(RelationModel rel, PrintWriter out) {
+    List<ColumnModel> cols = sortColumns(rel.getFields());
+    for (ColumnModel c : cols) {
+      generateMessage(c, out);
+    }
+
+    out.print("message " + getMessageName(rel) + " {\n");
+    for (ColumnModel c : cols) {
+      out.append("\toptional " + getType(c) + " " + getName(c) + " = "
+          + c.getColumnID() + ";\n");
+    }
+    out.print("}\n\n");
+  }
+
+  private void generateMessage(ColumnModel parent, PrintWriter out) {
+    // Handle base cases
+    if (!parent.isNested()) {
+      return;
+    } else if (seen.contains(parent.getNestedClassName())) {
+      return;
+    }
+
+    List<ColumnModel> children = sortColumns(parent.getNestedColumns());
+    for (ColumnModel child : children) {
+      generateMessage(child, out);
+    }
+
+    out.print("message " + getType(parent) + " {\n");
+    for (ColumnModel child : children) {
+      out.append("\toptional " + getType(child) + " " + getName(child) + " = "
+          + child.getColumnID() + ";\n");
+    }
+    out.print("}\n\n");
+
+    seen.add(parent.getNestedClassName());
+  }
+
+  private String getType(ColumnModel cm) {
+    if (cm.isNested()) {
+      String type = getShortClassName(cm);
+      if (collisions.contains(type)) {
+        return cm.getNestedClassName().replace('.', '_').replace('$', '_');
+      } else {
+        return type;
+      }
+    } else {
+      return toProtoType(cm.getPrimitiveType());
+    }
+  }
+
+  private static String getName(ColumnModel cm) {
+    if (cm.getColumnName().equals(Column.NONE)) {
+      return cm.getFieldName();
+    } else {
+      return cm.getColumnName();
+    }
+  }
+
+  private static String getShortClassName(ColumnModel cm) {
+    String tmp = cm.getNestedClassName();
+    return tmp.substring(tmp.lastIndexOf('.') + 1).replace('$', '_');
+  }
+
+  private static String getMessageName(RelationModel r) {
+    String typeName = r.getEntityTypeClassName();
+    return typeName.substring(typeName.lastIndexOf('.') + 1);
+  }
+
+  private static List<ColumnModel> sortColumns(Collection<ColumnModel> cols) {
+    ArrayList<ColumnModel> list = new ArrayList<ColumnModel>(cols);
+    Collections.sort(list, COLUMN_COMPARATOR);
+    return list;
+  }
+
+  private static List<RelationModel> sortRelations(
+      Collection<RelationModel> rels) {
+    ArrayList<RelationModel> list = new ArrayList<RelationModel>(rels);
+    Collections.sort(list, RELATION_COMPARATOR);
+    return list;
+  }
+
+  private static String toProtoType(Class<?> clazz) {
+    switch (Type.getType(clazz).getSort()) {
+      case Type.BOOLEAN:
+        return "bool";
+      case Type.CHAR:
+        return "uint32";
+      case Type.BYTE:
+      case Type.SHORT:
+      case Type.INT:
+        return "sint32";
+      case Type.FLOAT:
+        return "float";
+      case Type.DOUBLE:
+        return "double";
+      case Type.LONG:
+        return "sint64";
+      case Type.ARRAY:
+      case Type.OBJECT: {
+        if (clazz == byte[].class) {
+          return "bytes";
+        } else if (clazz == String.class) {
+          return "string";
+        } else if (clazz == java.sql.Timestamp.class) {
+          return "fixed64";
+        } else {
+          throw new RuntimeException("Type " + clazz
+              + " not supported on protobuf!");
+        }
+      }
+
+      default:
+        throw new RuntimeException("Type " + clazz
+            + " not supported on protobuf!");
+    }
+  }
+}