Refactor SchemaGen to be reused for NoSQL backends

The entire schema implementation is just about constructing the
various methods that return the Access implementations for each
relation.  This isn't unique to JDBC and can easily be reused
for a NoSQL backend as-is, if we properly abstract out the base
class it uses and the method to obtain each Access implementation
as its required by the schema code generator.

Change-Id: If9b1d63b1b83fafb1eab14b64861cd26dde73d2e
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/jdbc/Database.java b/src/main/java/com/google/gwtorm/jdbc/Database.java
index aa48022..976998b 100644
--- a/src/main/java/com/google/gwtorm/jdbc/Database.java
+++ b/src/main/java/com/google/gwtorm/jdbc/Database.java
@@ -14,11 +14,13 @@
 
 package com.google.gwtorm.jdbc;
 
+import com.google.gwtorm.client.Access;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.Schema;
 import com.google.gwtorm.client.SchemaFactory;
-import com.google.gwtorm.jdbc.gen.SchemaGen;
+import com.google.gwtorm.jdbc.gen.AccessGen;
+import com.google.gwtorm.schema.RelationModel;
 import com.google.gwtorm.schema.SchemaModel;
 import com.google.gwtorm.schema.java.JavaSchemaModel;
 import com.google.gwtorm.schema.sql.DialectH2;
@@ -27,6 +29,7 @@
 import com.google.gwtorm.schema.sql.SqlDialect;
 import com.google.gwtorm.server.GeneratedClassLoader;
 import com.google.gwtorm.server.SchemaConstructorGen;
+import com.google.gwtorm.server.SchemaGen;
 import com.google.gwtorm.server.StandardKeyEncoder;
 
 import java.sql.Connection;
@@ -105,9 +108,17 @@
   }
 
   @SuppressWarnings("unchecked")
-  private Class<T> generate(SqlDialect dialect,
+  private Class<T> generate(final SqlDialect dialect,
       final GeneratedClassLoader loader) throws OrmException {
-    return (Class<T>) new SchemaGen(loader, schemaModel, dialect).create();
+    return new SchemaGen(loader, schemaModel, JdbcSchema.class,
+        new SchemaGen.AccessGenerator() {
+          @Override
+          public <A extends Access<?, ?>> Class<A> create(
+              GeneratedClassLoader loader, RelationModel rm)
+              throws OrmException {
+            return new AccessGen(loader, rm, dialect).create();
+          }
+        }).create();
   }
 
   SqlDialect getDialect() {
diff --git a/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java b/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java
index f217455..f8496de 100644
--- a/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java
+++ b/src/main/java/com/google/gwtorm/jdbc/gen/AccessGen.java
@@ -61,7 +61,6 @@
   }
 
   private final GeneratedClassLoader classLoader;
-  private final SchemaGen.RelationGen info;
   private final RelationModel model;
   private final SqlDialect dialect;
 
@@ -73,16 +72,15 @@
 
 
   public AccessGen(final GeneratedClassLoader loader,
-      final SchemaGen.RelationGen ri) {
+      final RelationModel rm, final SqlDialect sd) {
     classLoader = loader;
-    info = ri;
-    model = info.model;
-    dialect = ri.getDialect();
+    model = rm;
+    dialect = sd;
     entityType =
         Type.getObjectType(model.getEntityTypeClassName().replace('.', '/'));
   }
 
-  public void defineClass() throws OrmException {
+  public <A extends Access<?, ?>> Class<A> create() throws OrmException {
     init();
     implementConstructor();
     implementGetString("getRelationName", model.getRelationName());
@@ -123,9 +121,18 @@
 
     cw.visitEnd();
     classLoader.defineClass(implClassName, cw.toByteArray());
-    info.accessClassName = implClassName;
+    return loadClass();
   }
 
+  @SuppressWarnings("unchecked")
+  private <A extends Access<?, ?>> Class<A> loadClass() throws OrmException {
+    try {
+      final Class<?> c = Class.forName(implClassName, false, classLoader);
+      return (Class<A>) c;
+    } catch (ClassNotFoundException err) {
+      throw new OrmException("Cannot load generated class", err);
+    }
+  }
 
   private void init() {
     superTypeName = Type.getInternalName(JdbcAccess.class);
diff --git a/src/main/java/com/google/gwtorm/jdbc/gen/SchemaGen.java b/src/main/java/com/google/gwtorm/server/SchemaGen.java
similarity index 81%
rename from src/main/java/com/google/gwtorm/jdbc/gen/SchemaGen.java
rename to src/main/java/com/google/gwtorm/server/SchemaGen.java
index fc46f9a..d6aa052 100644
--- a/src/main/java/com/google/gwtorm/jdbc/gen/SchemaGen.java
+++ b/src/main/java/com/google/gwtorm/server/SchemaGen.java
@@ -12,8 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gwtorm.jdbc.gen;
+package com.google.gwtorm.server;
 
+import com.google.gwtorm.client.Access;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.Schema;
 import com.google.gwtorm.jdbc.Database;
@@ -22,8 +23,6 @@
 import com.google.gwtorm.schema.SequenceModel;
 import com.google.gwtorm.schema.Util;
 import com.google.gwtorm.schema.java.JavaSchemaModel;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.GeneratedClassLoader;
 
 import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.MethodVisitor;
@@ -34,21 +33,28 @@
 import java.util.List;
 
 /** Generates a concrete implementation of a {@link Schema} extension. */
-public class SchemaGen implements Opcodes {
+public class SchemaGen<S extends AbstractSchema> implements Opcodes {
+  public interface AccessGenerator {
+    <A extends Access<?, ?>> Class<A> create(GeneratedClassLoader loader,
+        RelationModel rm) throws OrmException;
+  }
+
   private final GeneratedClassLoader classLoader;
   private final JavaSchemaModel schema;
-  private final SqlDialect dialect;
+  private final Class<S> schemaSuperClass;
+  private final AccessGenerator accessGen;
   private List<RelationGen> relations;
   private ClassWriter cw;
-  private String superTypeName;
   private String implClassName;
   private String implTypeName;
 
   public SchemaGen(final GeneratedClassLoader loader,
-      final JavaSchemaModel schemaModel, final SqlDialect sqlDialect) {
+      final JavaSchemaModel schemaModel, final Class<S> superType,
+      final AccessGenerator ag) {
     classLoader = loader;
     schema = schemaModel;
-    dialect = sqlDialect;
+    schemaSuperClass = superType;
+    accessGen = ag;
   }
 
   public Class<Schema> create() throws OrmException {
@@ -89,20 +95,19 @@
   private void defineRelationClasses() throws OrmException {
     relations = new ArrayList<RelationGen>();
     for (final RelationModel rel : schema.getRelations()) {
-      final RelationGen g = new RelationGen(rel);
-      relations.add(g);
-      new AccessGen(classLoader, g).defineClass();
+      final Class<? extends Access> a = accessGen.create(classLoader, rel);
+      relations.add(new RelationGen(rel, a));
     }
   }
 
   private void init() {
-    superTypeName = Type.getInternalName(JdbcSchema.class);
     implClassName = getSchemaClassName() + "_Schema_" + 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[] {getSchemaClassName().replace('.', '/')});
+    cw.visit(V1_3, ACC_PUBLIC | ACC_FINAL | ACC_SUPER, implTypeName, null, Type
+        .getInternalName(schemaSuperClass), new String[] {getSchemaClassName()
+        .replace('.', '/')});
   }
 
   private void implementRelationFields() {
@@ -122,7 +127,8 @@
 
     mv.visitVarInsn(ALOAD, 0);
     mv.visitVarInsn(ALOAD, 1);
-    mv.visitMethodInsn(INVOKESPECIAL, superTypeName, consName, consDesc);
+    mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(schemaSuperClass),
+        consName, consDesc);
 
     for (final RelationGen info : relations) {
       mv.visitVarInsn(ALOAD, 0);
@@ -154,9 +160,9 @@
 
       mv.visitVarInsn(ALOAD, 0);
       mv.visitLdcInsn(seq.getSequenceName());
-      mv.visitMethodInsn(INVOKEVIRTUAL, superTypeName, "nextLong", Type
-          .getMethodDescriptor(Type.getType(Long.TYPE), new Type[] {Type
-              .getType(String.class)}));
+      mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(schemaSuperClass),
+          "nextLong", Type.getMethodDescriptor(Type.getType(Long.TYPE),
+              new Type[] {Type.getType(String.class)}));
       if (retType.getSize() == 1) {
         mv.visitInsn(L2I);
         mv.visitInsn(IRETURN);
@@ -174,21 +180,16 @@
     }
   }
 
-  class RelationGen {
+  private class RelationGen {
     final RelationModel model;
-    String accessClassName;
-    Type accessType;
+    final Type accessType;
 
-    RelationGen(final RelationModel model) {
+    RelationGen(final RelationModel model, final Class<?> accessClass) {
       this.model = model;
-    }
-
-    SqlDialect getDialect() {
-      return SchemaGen.this.dialect;
+      this.accessType = Type.getType(accessClass);
     }
 
     void implementField() {
-      accessType = Type.getObjectType(accessClassName.replace('.', '/'));
       cw.visitField(ACC_PRIVATE | ACC_FINAL, getAccessInstanceFieldName(),
           accessType.getDescriptor(), null, null).visitEnd();
     }