| // 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.client.Key; |
| import com.google.gwtorm.protobuf.CodecFactory; |
| import com.google.gwtorm.protobuf.ProtobufCodec; |
| import com.google.gwtorm.schema.ColumnModel; |
| import com.google.gwtorm.schema.KeyModel; |
| import com.google.gwtorm.schema.QueryModel; |
| import com.google.gwtorm.schema.QueryParser; |
| import com.google.gwtorm.schema.RelationModel; |
| import com.google.gwtorm.schema.Util; |
| import com.google.gwtorm.server.CodeGenSupport; |
| import com.google.gwtorm.server.GeneratedClassLoader; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.gwtorm.server.ResultSet; |
| |
| import org.antlr.runtime.tree.Tree; |
| import org.objectweb.asm.ClassWriter; |
| import org.objectweb.asm.MethodVisitor; |
| import org.objectweb.asm.Opcodes; |
| import org.objectweb.asm.Type; |
| |
| import java.lang.reflect.Field; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** Generates a concrete implementation of a {@link NoSqlAccess} extension. */ |
| class AccessGen implements Opcodes { |
| private static final Type string = Type.getType(String.class); |
| private static final Type protobufCodec = Type.getType(ProtobufCodec.class); |
| private static final Type indexFunction = Type.getType(IndexFunction.class); |
| private static final Type object = Type.getType(Object.class); |
| private static final Type ormKey = Type.getType(Key.class); |
| private static final Type byteArray = Type.getType(byte[].class); |
| private static final Type ormException = Type.getType(OrmException.class); |
| private static final Type resultSet = Type.getType(ResultSet.class); |
| private static final Type indexKeyBuilder = |
| Type.getType(IndexKeyBuilder.class); |
| |
| private static final String F_OBJECT_CODEC = "objectCodec"; |
| private static final String F_INDEXES = "indexes"; |
| |
| private final GeneratedClassLoader classLoader; |
| private final RelationModel model; |
| private final Class<?> modelClass; |
| private final Type schemaType; |
| private final Type accessType; |
| private final Type entityType; |
| private final KeyModel key; |
| |
| private ClassWriter cw; |
| private String implClassName; |
| private String implTypeName; |
| |
| AccessGen(final GeneratedClassLoader loader, final RelationModel rm, |
| final Class<? extends NoSqlSchema> schemaClazz, |
| @SuppressWarnings("rawtypes") |
| final Class<? extends NoSqlAccess> accessClazz) throws OrmException { |
| classLoader = loader; |
| model = rm; |
| |
| try { |
| modelClass = |
| Class.forName(model.getEntityTypeClassName(), true, classLoader); |
| } catch (ClassNotFoundException cnfe) { |
| throw new OrmException("Cannot locate model class", cnfe); |
| } |
| |
| schemaType = Type.getType(schemaClazz); |
| accessType = Type.getType(accessClazz); |
| entityType = Type.getType(modelClass); |
| |
| key = model.getPrimaryKey(); |
| if (key == null) { |
| throw new OrmException("Relation " + rm.getMethodName() |
| + " has no primary key"); |
| } |
| } |
| |
| Class<?> create() throws OrmException { |
| init(); |
| implementStaticFields(); |
| implementConstructor(); |
| implementGetString("getRelationName", model.getRelationName()); |
| implementGetRelationID(); |
| implementGetObjectCodec(); |
| implementGetIndexes(); |
| |
| implementPrimaryKey(); |
| implementEncodePrimaryKey(); |
| implementKeyQuery(key); |
| |
| for (final QueryModel q : model.getQueries()) { |
| implementQuery(q); |
| } |
| implementQuery(new QueryModel(model, "iterateAllEntities", "")); |
| |
| cw.visitEnd(); |
| classLoader.defineClass(implClassName, cw.toByteArray()); |
| |
| final Class<?> c = loadClass(); |
| initObjectCodec(c); |
| initQueryIndexes(c); |
| return c; |
| } |
| |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| 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, oc); |
| } catch (IllegalArgumentException err) { |
| throw new OrmException("Cannot setup ProtobufCodec", err); |
| } catch (IllegalStateException err) { |
| throw new OrmException("Cannot setup ProtobufCodec", err); |
| } catch (IllegalAccessException err) { |
| throw new OrmException("Cannot setup ProtobufCodec", err); |
| } catch (SecurityException err) { |
| throw new OrmException("Cannot setup ProtobufCodec", err); |
| } catch (NoSuchFieldException err) { |
| throw new OrmException("Cannot setup ProtobufCodec", err); |
| } |
| } |
| |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| private void initQueryIndexes(final Class<?> clazz) throws OrmException { |
| final Collection<QueryModel> queries = model.getQueries(); |
| final ArrayList<IndexFunction> indexes = new ArrayList<IndexFunction>(); |
| for (QueryModel m : queries) { |
| if (needsIndexFunction(m)) { |
| indexes.add(new IndexFunctionGen(classLoader, m, modelClass).create()); |
| } |
| } |
| |
| try { |
| Field e = clazz.getDeclaredField(F_INDEXES); |
| e.setAccessible(true); |
| e.set(null, indexes.toArray(new IndexFunction[indexes.size()])); |
| |
| for (IndexFunction<?> f : indexes) { |
| e = clazz.getDeclaredField("index_" + f.getName()); |
| e.setAccessible(true); |
| e.set(null, f); |
| } |
| } catch (IllegalArgumentException err) { |
| throw new OrmException("Cannot setup query IndexFunctions", err); |
| } catch (IllegalStateException err) { |
| throw new OrmException("Cannot setup query IndexFunctions", err); |
| } catch (IllegalAccessException err) { |
| throw new OrmException("Cannot setup query IndexFunctions", err); |
| } catch (SecurityException err) { |
| throw new OrmException("Cannot setup query IndexFunctions", err); |
| } catch (NoSuchFieldException err) { |
| throw new OrmException("Cannot setup query IndexFunctions", err); |
| } |
| } |
| |
| private Class<?> loadClass() throws OrmException { |
| try { |
| return Class.forName(implClassName, false, classLoader); |
| } catch (ClassNotFoundException err) { |
| throw new OrmException("Cannot load generated class", err); |
| } |
| } |
| |
| private void init() { |
| implClassName = |
| model.getEntityTypeClassName() + "_Access_" + model.getMethodName() |
| + "_" + Util.createRandomName(); |
| implTypeName = implClassName.replace('.', '/'); |
| |
| cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); |
| cw.visit(V1_3, ACC_PUBLIC | ACC_FINAL | ACC_SUPER, implTypeName, null, |
| accessType.getInternalName(), new String[] {model |
| .getAccessInterfaceName().replace('.', '/')}); |
| } |
| |
| private void implementStaticFields() { |
| cw.visitField(ACC_PRIVATE | ACC_STATIC, F_OBJECT_CODEC, |
| protobufCodec.getDescriptor(), null, null).visitEnd(); |
| cw.visitField(ACC_PRIVATE | ACC_STATIC, F_INDEXES, |
| Type.getType(IndexFunction[].class).getDescriptor(), null, null) |
| .visitEnd(); |
| |
| for (final QueryModel q : model.getQueries()) { |
| if (needsIndexFunction(q)) { |
| cw.visitField(ACC_PRIVATE | ACC_STATIC, "index_" + q.getName(), |
| indexFunction.getDescriptor(), null, null).visitEnd(); |
| } |
| } |
| } |
| |
| private void implementConstructor() { |
| final String consName = "<init>"; |
| final String consDesc = |
| Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {schemaType}); |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC, consName, consDesc, null, null); |
| mv.visitCode(); |
| mv.visitVarInsn(ALOAD, 0); |
| mv.visitVarInsn(ALOAD, 1); |
| mv.visitMethodInsn(INVOKESPECIAL, accessType.getInternalName(), consName, |
| consDesc); |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementGetString(final String methodName, |
| final String returnValue) { |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, methodName, Type |
| .getMethodDescriptor(string, new Type[] {}), null, null); |
| mv.visitCode(); |
| mv.visitLdcInsn(returnValue); |
| mv.visitInsn(ARETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementGetRelationID() { |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, "getRelationID", Type |
| .getMethodDescriptor(Type.INT_TYPE, new Type[] {}), null, null); |
| mv.visitCode(); |
| new CodeGenSupport(mv).push(model.getRelationID()); |
| mv.visitInsn(IRETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementGetObjectCodec() { |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, "getObjectCodec", Type |
| .getMethodDescriptor(protobufCodec, new Type[] {}), null, null); |
| mv.visitCode(); |
| mv.visitFieldInsn(GETSTATIC, implTypeName, F_OBJECT_CODEC, protobufCodec |
| .getDescriptor()); |
| mv.visitInsn(ARETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementGetIndexes() { |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, "getIndexes", Type |
| .getMethodDescriptor(Type.getType(IndexFunction[].class), |
| new Type[] {}), null, null); |
| mv.visitCode(); |
| mv.visitFieldInsn(GETSTATIC, implTypeName, F_INDEXES, Type.getType( |
| IndexFunction[].class).getDescriptor()); |
| mv.visitInsn(ARETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementPrimaryKey() { |
| final ColumnModel f = key.getField(); |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, "primaryKey", Type |
| .getMethodDescriptor(ormKey, new Type[] {object}), null, null); |
| mv.visitCode(); |
| mv.visitVarInsn(ALOAD, 1); |
| mv.visitTypeInsn(CHECKCAST, entityType.getInternalName()); |
| mv.visitFieldInsn(GETFIELD, entityType.getInternalName(), f.getFieldName(), |
| CodeGenSupport.toType(f).getDescriptor()); |
| mv.visitInsn(ARETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementEncodePrimaryKey() throws OrmException { |
| final List<ColumnModel> pCols = Collections.singletonList(key.getField()); |
| final Type argType = CodeGenSupport.toType(key.getField()); |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, "encodePrimaryKey", Type |
| .getMethodDescriptor(Type.VOID_TYPE, new Type[] {indexKeyBuilder, |
| ormKey}), null, null); |
| mv.visitCode(); |
| |
| mv.visitVarInsn(ALOAD, 2); |
| mv.visitTypeInsn(CHECKCAST, argType.getInternalName()); |
| mv.visitVarInsn(ASTORE, 2); |
| |
| final QueryCGS cgs = |
| new QueryCGS(mv, new Type[] {argType}, pCols, new int[] {2}, 1); |
| for (ColumnModel f : pCols) { |
| IndexFunctionGen.encodeField(new QueryModel.OrderBy(f, false), mv, cgs); |
| } |
| |
| mv.visitInsn(RETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementKeyQuery(KeyModel key) { |
| final Type keyType = CodeGenSupport.toType(key.getField()); |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, key.getName(), Type |
| .getMethodDescriptor(entityType, new Type[] {keyType}), null, |
| new String[] {Type.getType(OrmException.class).getInternalName()}); |
| mv.visitCode(); |
| |
| mv.visitVarInsn(ALOAD, 0); |
| mv.visitVarInsn(ALOAD, 1); |
| mv.visitMethodInsn(INVOKESPECIAL, accessType.getInternalName(), "get", Type |
| .getMethodDescriptor(object, new Type[] {ormKey})); |
| mv.visitTypeInsn(CHECKCAST, entityType.getInternalName()); |
| |
| mv.visitInsn(ARETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private void implementQuery(final QueryModel info) throws OrmException { |
| final List<ColumnModel> pCols = info.getParameters(); |
| final boolean hasLimitParam = info.hasLimitParameter(); |
| final Type[] pTypes = new Type[pCols.size() + (hasLimitParam ? 1 : 0)]; |
| final int[] pVars = new int[pTypes.length]; |
| int nextVar = 1; |
| for (int i = 0; i < pCols.size(); i++) { |
| pTypes[i] = CodeGenSupport.toType(pCols.get(i)); |
| pVars[i] = nextVar; |
| nextVar += pTypes[i].getSize(); |
| } |
| if (hasLimitParam) { |
| pTypes[pTypes.length - 1] = Type.INT_TYPE; |
| pVars[pTypes.length - 1] = nextVar; |
| nextVar += Type.INT_TYPE.getSize(); |
| } |
| |
| final MethodVisitor mv = |
| cw.visitMethod(ACC_PUBLIC | ACC_FINAL, info.getName(), Type |
| .getMethodDescriptor(resultSet, pTypes), null, |
| new String[] {ormException.getInternalName()}); |
| mv.visitCode(); |
| |
| final List<Tree> ops = compareOpsOnly(info.getParseTree()); |
| |
| // Generate fromKey |
| // |
| final int fromBuf = nextVar++; |
| mv.visitTypeInsn(NEW, indexKeyBuilder.getInternalName()); |
| mv.visitInsn(DUP); |
| mv.visitMethodInsn(INVOKESPECIAL, indexKeyBuilder.getInternalName(), |
| "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {})); |
| mv.visitVarInsn(ASTORE, fromBuf); |
| |
| QueryCGS cgs = new QueryCGS(mv, pTypes, pCols, pVars, fromBuf); |
| encodeFields(info, ops, mv, cgs, true /* fromKey */); |
| |
| // Generate toKey |
| // |
| final int toBuf = nextVar++; |
| mv.visitTypeInsn(NEW, indexKeyBuilder.getInternalName()); |
| mv.visitInsn(DUP); |
| mv.visitMethodInsn(INVOKESPECIAL, indexKeyBuilder.getInternalName(), |
| "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {})); |
| mv.visitVarInsn(ASTORE, toBuf); |
| |
| cgs = new QueryCGS(mv, pTypes, pCols, pVars, toBuf); |
| encodeFields(info, ops, mv, cgs, false /* fromKey */); |
| cgs.infinity(); |
| |
| // Make the scan call |
| // |
| mv.visitVarInsn(ALOAD, 0); |
| if (needsIndexFunction(info)) { |
| mv.visitFieldInsn(GETSTATIC, implTypeName, "index_" + info.getName(), |
| indexFunction.getDescriptor()); |
| } |
| |
| mv.visitVarInsn(ALOAD, fromBuf); |
| mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(), |
| "toByteArray", Type.getMethodDescriptor(byteArray, new Type[] {})); |
| |
| mv.visitVarInsn(ALOAD, toBuf); |
| mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(), |
| "toByteArray", Type.getMethodDescriptor(byteArray, new Type[] {})); |
| |
| // Set the limit on the number of results. |
| // |
| if (info.hasLimit()) { |
| if (hasLimitParam) { |
| mv.visitVarInsn(ILOAD, pVars[pTypes.length - 1]); |
| } else { |
| cgs.push(info.getStaticLimit()); |
| } |
| } else { |
| cgs.push(0); |
| } |
| |
| // Only keep order if there is an order by clause present |
| // |
| cgs.push(info.hasOrderBy() ? 1 : 0); |
| |
| if (needsIndexFunction(info)) { |
| mv.visitMethodInsn(INVOKEVIRTUAL, accessType.getInternalName(), |
| "scanIndex", Type.getMethodDescriptor(resultSet, new Type[] { |
| indexFunction, byteArray, byteArray, Type.INT_TYPE, |
| Type.BOOLEAN_TYPE})); |
| } else { |
| // No where and no order by clause? Use the primary key instead. |
| // |
| mv.visitMethodInsn(INVOKEVIRTUAL, accessType.getInternalName(), |
| "scanPrimaryKey", Type.getMethodDescriptor(resultSet, new Type[] { |
| byteArray, byteArray, Type.INT_TYPE, Type.BOOLEAN_TYPE})); |
| } |
| |
| mv.visitInsn(ARETURN); |
| mv.visitMaxs(-1, -1); |
| mv.visitEnd(); |
| } |
| |
| private boolean needsIndexFunction(final QueryModel info) { |
| return info.hasWhere() || info.hasOrderBy(); |
| } |
| |
| private void encodeFields(QueryModel qm, List<Tree> query, MethodVisitor mv, |
| QueryCGS cgs, boolean fromKey) throws OrmException { |
| final boolean toKey = !fromKey; |
| Tree lastNode = null; |
| |
| for (Tree node : query) { |
| switch (node.getType()) { |
| case QueryParser.GE: |
| if (fromKey) { |
| checkLastNode(qm, lastNode); |
| encodeField(node, mv, cgs); |
| cgs.delimiter(); |
| lastNode = node; |
| } |
| break; |
| |
| case QueryParser.GT: |
| if (fromKey) { |
| checkLastNode(qm, lastNode); |
| encodeField(node, mv, cgs); |
| cgs.delimiter(); |
| cgs.infinity(); |
| lastNode = node; |
| } |
| break; |
| |
| case QueryParser.EQ: |
| checkLastNode(qm, lastNode); |
| encodeField(node, mv, cgs); |
| cgs.delimiter(); |
| break; |
| |
| case QueryParser.LE: |
| if (toKey) { |
| checkLastNode(qm, lastNode); |
| encodeField(node, mv, cgs); |
| cgs.delimiter(); |
| lastNode = node; |
| } |
| break; |
| |
| case QueryParser.LT: |
| if (toKey) { |
| checkLastNode(qm, lastNode); |
| encodeField(node, mv, cgs); |
| cgs.delimiter(); |
| cgs.nul(); |
| lastNode = node; |
| } |
| break; |
| |
| default: |
| throw new OrmException("Unsupported query token in " |
| + model.getMethodName() + "." + qm.getName() + ": " |
| + node.toStringTree()); |
| } |
| |
| cgs.nextParameter(); |
| } |
| } |
| |
| private void checkLastNode(QueryModel qm, Tree lastNode) throws OrmException { |
| if (lastNode != null) { |
| throw new OrmException(lastNode.getText() + " must be last operator in " |
| + model.getMethodName() + "." + qm.getName()); |
| } |
| } |
| |
| private void encodeField(Tree node, MethodVisitor mv, QueryCGS cgs) |
| throws OrmException { |
| ColumnModel f = ((QueryParser.Column) node.getChild(0)).getField(); |
| IndexFunctionGen.encodeField(new QueryModel.OrderBy(f, false), mv, cgs); |
| } |
| |
| private List<Tree> compareOpsOnly(Tree node) throws OrmException { |
| if (node == null) { |
| return Collections.emptyList(); |
| } |
| |
| switch (node.getType()) { |
| case 0: // nil node used to join other nodes together |
| case QueryParser.WHERE: |
| case QueryParser.AND: { |
| List<Tree> res = new ArrayList<Tree>(); |
| for (int i = 0; i < node.getChildCount(); i++) { |
| res.addAll(compareOpsOnly(node.getChild(i))); |
| } |
| return res; |
| } |
| |
| case QueryParser.GT: |
| case QueryParser.GE: |
| case QueryParser.EQ: |
| case QueryParser.LE: |
| case QueryParser.LT: { |
| final Tree lhs = node.getChild(0); |
| final Tree rhs = node.getChild(1); |
| if (lhs.getType() != QueryParser.ID) { |
| throw new OrmException("Unsupported query token"); |
| } |
| if (rhs.getType() == QueryParser.PLACEHOLDER) { |
| return Collections.singletonList(node); |
| } |
| break; |
| } |
| |
| case QueryParser.ORDER: |
| case QueryParser.LIMIT: |
| break; |
| |
| default: |
| throw new OrmException("Unsupported query token " + node.toStringTree()); |
| } |
| return Collections.emptyList(); |
| } |
| |
| private final class QueryCGS extends IndexFunctionGen.EncodeCGS { |
| private final Type[] pTypes; |
| private final List<ColumnModel> pCols; |
| private final int[] pVars; |
| private final int bufvar; |
| private int currentp; |
| |
| private QueryCGS(MethodVisitor method, Type[] pTypes, |
| List<ColumnModel> pCols, int[] pVars, int bufvar) { |
| super(method); |
| this.pTypes = pTypes; |
| this.pCols = pCols; |
| this.pVars = pVars; |
| this.bufvar = bufvar; |
| } |
| |
| void nextParameter() { |
| currentp++; |
| } |
| |
| @Override |
| void pushBuilder() { |
| mv.visitVarInsn(ALOAD, bufvar); |
| } |
| |
| @Override |
| public void pushFieldValue() { |
| appendGetField(getFieldReference()); |
| } |
| |
| @Override |
| protected void appendGetField(final ColumnModel c) { |
| if (currentp < pTypes.length && pCols.get(currentp).equals(c)) { |
| loadVar(pTypes[currentp], pVars[currentp]); |
| } else { |
| super.appendGetField(c); |
| } |
| } |
| } |
| } |