blob: c84fab5cdf83920b6726c3089732a3c6a2eaade5 [file] [log] [blame]
// 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 java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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;
/** 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<>();
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<>();
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 static 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);
}
}
}
}