From 74a8f56ab50d2483047ef42dc763ce67da15f35f Mon Sep 17 00:00:00 2001 From: apucher Date: Sun, 8 Jan 2023 14:45:03 +0100 Subject: [PATCH] wip --- .../jef/asm/access/ClassAnalyzerVisitor.java | 2 +- .../jef/asm/access/MethodAnalyzerVisitor.java | 68 +- .../ConstructorParameterDescription.java | 1 + .../java/jef/model/annotations/Generated.java | 2 +- .../mysql/dbinterface/ByteCodeHelper.java | 88 ++- .../dbinterface/MysqlEntityDbInterface.java | 65 +- .../MysqlEntityDbInterfaceGenerator.java | 669 ++++++++++++++---- 7 files changed, 682 insertions(+), 213 deletions(-) diff --git a/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java b/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java index 175ac9d..fcec875 100644 --- a/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java +++ b/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java @@ -54,7 +54,7 @@ class ClassAnalyzerVisitor extends ClassVisitor { var type = Type.getMethodType(descriptor); var returnClassName = type.getReturnType().getClassName(); var parameterClassNames = Arrays.stream(type.getArgumentTypes()).map(Type::getClassName).toArray(String[]::new); - return new MethodAnalyzerVisitor(Opcodes.ASM9, className, name, returnClassName, parameterClassNames, entityAccess -> { + return new MethodAnalyzerVisitor(Opcodes.ASM9, className, name, access, returnClassName, parameterClassNames, entityAccess -> { declaredConstructor.addAll(entityAccess.getDeclaredConstructors()); declaredGetters.addAll(entityAccess.getDeclaredGetters()); declaredSetters.addAll(entityAccess.getDeclaredSetters()); diff --git a/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java b/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java index 250aba0..d9a137c 100644 --- a/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java +++ b/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java @@ -6,6 +6,7 @@ import jef.asm.access.model.ConstructorParameterDescription; import jef.asm.access.model.GetterDescription; import jef.asm.access.model.SetterDescription; import jef.asm.access.model.SuperConstructorParameterDescription; +import lombok.AllArgsConstructor; import lombok.Getter; import org.objectweb.asm.Handle; import org.objectweb.asm.Label; @@ -14,6 +15,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.Stack; @@ -22,14 +24,15 @@ import java.util.function.Consumer; @Getter public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param count private final Consumer callback; - private final String methodClassName; + private final String declaringClassName; private final String methodName; + private final int access; private final String returnClassName; - private final String[] parameterClassNames; + private final String[] parameterClassNames; private List constructorParameterDescriptions = new ArrayList<>(); private List superConstructorParameterDescriptions = new ArrayList<>(); - private Stack varIndexStack = new Stack<>(); + private Stack varStack = new Stack<>(); private String fieldDeclaringClassName = null; private String fieldName = null; @@ -69,12 +72,19 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co INVALID } - protected MethodAnalyzerVisitor(int api, String methodClassName, String methodName, String returnClassName, String[] parameterClassNames, Consumer callback) { + protected MethodAnalyzerVisitor(int api, String declaringClassName, String methodName, int access, String returnClassName, String[] parameterClassNames, Consumer callback) { super(api); - this.methodClassName = methodClassName; + this.declaringClassName = declaringClassName; this.methodName = methodName; + this.access = access; this.returnClassName = returnClassName; - this.parameterClassNames = parameterClassNames; + if (hasThis()) { + this.parameterClassNames = new String[parameterClassNames.length + 1]; + System.arraycopy(parameterClassNames, 0, this.parameterClassNames, 1, parameterClassNames.length); + this.parameterClassNames[0] = declaringClassName; + } else { + this.parameterClassNames = parameterClassNames; + } this.callback = callback; } @@ -122,7 +132,11 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co @Override public void visitVarInsn(int opcode, int varIndex) { if (Set.of(Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.ALOAD).contains(opcode)) { - varIndexStack.push(varIndex); + System.out.println(methodName + ": load_" + varIndex + ": " + (varIndex < parameterClassNames.length ? parameterClassNames[varIndex] : null)); + if (varIndex == 5) { + int x = 0; + } + varStack.push(new Var(varIndex < parameterClassNames.length ? parameterClassNames[varIndex] : null, varIndex)); } if (opcode == Opcodes.ALOAD && varIndex == 0) { if (constructorStep == ConstructorSteps.CODE) { @@ -160,6 +174,10 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co super.visitVarInsn(opcode, varIndex); } + private boolean hasThis() { + return (access & Opcodes.ACC_STATIC) == 0; + } + @Override public void visitTypeInsn(int opcode, String type) { constructorStep = ConstructorSteps.INVALID; @@ -170,15 +188,15 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { - var lastLoadedVarIndex = -1; + Var lastLoadedVar = null; if (opcode == Opcodes.PUTFIELD) { - lastLoadedVarIndex = varIndexStack.pop(); - varIndexStack.pop(); + lastLoadedVar = varStack.pop(); + varStack.pop(); constructorStep = constructorStep == ConstructorSteps.VAR_LOAD ? ConstructorSteps.PUT_FIELD : ConstructorSteps.INVALID; getterStep = GetterSteps.INVALID; setterStep = setterStep == SetterSteps.VAR_LOAD ? SetterSteps.PUT_FIELD : SetterSteps.INVALID; } else if (opcode == Opcodes.GETFIELD) { - lastLoadedVarIndex = varIndexStack.pop(); + lastLoadedVar = varStack.pop(); constructorStep = ConstructorSteps.INVALID; getterStep = getterStep == GetterSteps.ALOAD_0 ? GetterSteps.GET_FIELD : GetterSteps.INVALID; setterStep = SetterSteps.INVALID; @@ -189,21 +207,21 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co } this.fieldDeclaringClassName = owner.replace("/", "."); this.fieldName = name; - this.constructorParameterDescriptions.add(new ConstructorParameterDescription(lastLoadedVarIndex - 1, this.fieldDeclaringClassName, this.fieldName)); //lastLoadedVarIndex - 1 because arg 0 of an instance function is always this + this.constructorParameterDescriptions.add(new ConstructorParameterDescription(lastLoadedVar.getIndex() - 1, lastLoadedVar.getClassName(), this.fieldDeclaringClassName, this.fieldName)); //lastLoadedVar - 1 because arg 0 of an instance function is always this super.visitFieldInsn(opcode, owner, name, descriptor); } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - var argIndexes = new ArrayList(); + var args = new ArrayList(); for (var i = 0; i < Type.getArgumentTypes(descriptor).length; i++) { - argIndexes.add(0, varIndexStack.pop()); + args.add(0, varStack.pop()); } - varIndexStack.pop(); // also pop ALOAD0 + varStack.pop(); // also pop ALOAD0 if (opcode == Opcodes.INVOKESPECIAL && name.equals("") //TODO does it have to be called '' or is this just convention - && Set.of(ConstructorSteps.PRE_SUPER_INIT_ALOAD_0, ConstructorSteps.PRE_SUPER_INIT_VAR_LOAD).contains(constructorStep)) { - for (int i = 0; i < argIndexes.size(); i++) { - superConstructorParameterDescriptions.add(new SuperConstructorParameterDescription(argIndexes.get(i), i)); + && Set.of(ConstructorSteps.PRE_SUPER_INIT_ALOAD_0, ConstructorSteps.PRE_SUPER_INIT_VAR_LOAD).contains(constructorStep)) { + for (int i = 0; i < args.size(); i++) { + superConstructorParameterDescriptions.add(new SuperConstructorParameterDescription(args.get(i).getIndex(), i)); } constructorStep = ConstructorSteps.SUPER_INIT; } else { @@ -232,9 +250,6 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co @Override public void visitLabel(Label label) { -// constructorStep = ConstructorSteps.INVALID; -// getterStep = GetterSteps.INVALID; -// setterStep = SetterSteps.INVALID; super.visitLabel(label); } @@ -300,9 +315,16 @@ public class MethodAnalyzerVisitor extends MethodVisitor {//TODO verify param co public void visitEnd() { super.visitEnd(); List constructors = constructorStep == ConstructorSteps.RETURN ? List.of(new ConstructorDescription(constructorParameterDescriptions, superConstructorParameterDescriptions)) : List.of(); - List getters = getterStep == GetterSteps.RETURN ? List.of(new GetterDescription(methodClassName, methodName, returnClassName, parameterClassNames, fieldDeclaringClassName, fieldName)) : List.of(); - List setters = setterStep == SetterSteps.RETURN ? List.of(new SetterDescription(methodClassName, methodName, returnClassName, parameterClassNames, fieldDeclaringClassName, fieldName)) : List.of(); + List getters = getterStep == GetterSteps.RETURN ? List.of(new GetterDescription(declaringClassName, methodName, returnClassName, Arrays.stream(parameterClassNames).skip(hasThis() ? 1 : 0).toArray(String[]::new), fieldDeclaringClassName, fieldName)) : List.of(); + List setters = setterStep == SetterSteps.RETURN ? List.of(new SetterDescription(declaringClassName, methodName, returnClassName, Arrays.stream(parameterClassNames).skip(hasThis() ? 1 : 0).toArray(String[]::new), fieldDeclaringClassName, fieldName)) : List.of(); var access = new ClassDescription(null, null, null, constructors, getters, setters); callback.accept(access); } + + @AllArgsConstructor + @Getter + private static class Var { + private final String className; + private final int index; + } } diff --git a/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java b/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java index 949785c..82d8b8f 100644 --- a/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java +++ b/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java @@ -9,6 +9,7 @@ import lombok.ToString; @ToString public class ConstructorParameterDescription { private final int parameterIndex; + private final String parameterClassName; private final String declaringClassName; private final String fieldName; } diff --git a/core/src/main/java/jef/model/annotations/Generated.java b/core/src/main/java/jef/model/annotations/Generated.java index 3ab3aed..7215a5b 100644 --- a/core/src/main/java/jef/model/annotations/Generated.java +++ b/core/src/main/java/jef/model/annotations/Generated.java @@ -15,7 +15,7 @@ public @interface Generated { enum Type { NONE,//no value is generated IDENTITY,//value is generated on insert -// COMPUTED,//value is generated on insert and update + COMPUTED,//value is generated on insert and update ; } } diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java index 3b0ef80..e0f70f5 100644 --- a/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java @@ -4,6 +4,7 @@ import jef.asm.access.model.ClassDescription; import jef.asm.access.model.GetterDescription; import jef.asm.access.model.SetterDescription; import jef.model.DbField; +import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -18,6 +19,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.Arrays; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -56,24 +58,15 @@ public abstract class ByteCodeHelper { "(" + Arrays.stream(getter.getMethodParameterClassNames()) .map(ByteCodeHelper::getDescriptorForClassName) .collect(Collectors.joining()) - + ")" + getDescriptorForClassName(getter.getMethodReturnClassName()), + + ")" + getDescriptorForClassName(getter.getMethodReturnClassName()), false); } - private static void visitGetViaReflection(MethodVisitor mv, Field field, int varIndexEntity, int varIndexField) { - // var field = EntityClass.class.getDeclaredField("fieldname"); - mv.visitLdcInsn(Type.getObjectType(field.getDeclaringClass().getName())); - mv.visitLdcInsn(field.getName()); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_CLASS.getInternalName(), "getDeclaredField", Type.getMethodDescriptor(T_FIELD, T_STRING), false); - mv.visitVarInsn(Opcodes.ASTORE, varIndexField); + private static void visitGetViaReflection(MethodVisitor mv, Field field, int varIndexEntity, String className, Runnable defineFieldCallback) { + defineFieldCallback.run(); - // field.setAccessible(true); - mv.visitVarInsn(Opcodes.ALOAD, varIndexField); - mv.visitInsn(Opcodes.ICONST_1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setAccessible", Type.getMethodDescriptor(T_VOID, T_BOOL), false); - - // field.get*(entity); - mv.visitVarInsn(Opcodes.ALOAD, varIndexField); + // field.get*(entity, value); + mv.visitFieldInsn(Opcodes.GETSTATIC, className.replace(".", "/"), makeReflectedFieldName(field), Type.getDescriptor(field.getType())); mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); var clazz = field.getType(); if (clazz == boolean.class) { @@ -97,7 +90,7 @@ public abstract class ByteCodeHelper { } } - static void visitGetField(MethodVisitor mv, Field field, int varIndexEntity, int varIndexField, ClassDescription classDescription) { + static void visitGetField(MethodVisitor mv, Field field, int varIndexEntity, String className, Runnable defineFieldCallback, ClassDescription classDescription) { //try direct member access if (Modifier.isPublic(field.getModifiers())) {//TODO better check if publicly accessible visitGetFieldDirectAccess(mv, field, varIndexEntity); @@ -107,7 +100,7 @@ public abstract class ByteCodeHelper { //try finding getter Optional getter = classDescription.getDeclaredGetters().stream() .filter(e -> e.getFieldDeclaringClassName().equals(field.getDeclaringClass().getName()) - && e.getFieldName().equals(field.getName())) + && e.getFieldName().equals(field.getName())) .findFirst(); if (getter.isPresent()) { visitGetter(mv, getter.get(), varIndexEntity); @@ -115,7 +108,7 @@ public abstract class ByteCodeHelper { } //use reflection - visitGetViaReflection(mv, field, varIndexEntity, varIndexField); + visitGetViaReflection(mv, field, varIndexEntity, className, defineFieldCallback); } //endregion @@ -135,24 +128,15 @@ public abstract class ByteCodeHelper { "(" + Arrays.stream(setter.getMethodParameterClassNames()) .map(ByteCodeHelper::getDescriptorForClassName) .collect(Collectors.joining()) - + ")" + getDescriptorForClassName(setter.getMethodReturnClassName()), + + ")" + getDescriptorForClassName(setter.getMethodReturnClassName()), false); } - private static void visitSetViaReflection(MethodVisitor mv, Field field, Consumer value, int varIndexEntity, int varIndexField) { - // var field = EntityClass.class.getDeclaredField("fieldname"); - mv.visitLdcInsn(Type.getObjectType(field.getDeclaringClass().getName())); - mv.visitLdcInsn(field.getName()); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_CLASS.getInternalName(), "getDeclaredField", Type.getMethodDescriptor(T_FIELD, T_STRING), false); - mv.visitVarInsn(Opcodes.ASTORE, varIndexField); - - // field.setAccessible(true); - mv.visitVarInsn(Opcodes.ALOAD, varIndexField); - mv.visitInsn(Opcodes.ICONST_1); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setAccessible", Type.getMethodDescriptor(T_VOID, T_BOOL), false); + private static void visitSetViaReflection(MethodVisitor mv, Field field, Consumer value, int varIndexEntity, String className, Runnable defineFieldCallback) { + defineFieldCallback.run(); // field.set*(entity, value); - mv.visitVarInsn(Opcodes.ALOAD, varIndexField); + mv.visitFieldInsn(Opcodes.GETSTATIC, className.replace(".", "/"), makeReflectedFieldName(field), Type.getDescriptor(field.getType())); mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); value.accept(mv); var clazz = field.getType(); @@ -177,7 +161,7 @@ public abstract class ByteCodeHelper { } } - static void visitSetField(MethodVisitor mv, Field field, Consumer value, int varIndexEntity, int varIndexField, ClassDescription classDescription) { + static void visitSetField(MethodVisitor mv, Field field, Consumer value, int varIndexEntity, String className, Runnable defineFieldCallback, ClassDescription classDescription) { //try direct member access if (Modifier.isPublic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers())) {//TODO better check if publicly accessible visitSetFieldDirectAccess(mv, field, value, varIndexEntity); @@ -187,7 +171,7 @@ public abstract class ByteCodeHelper { //try finding getter Optional setter = classDescription.getDeclaredSetters().stream() .filter(e -> e.getFieldDeclaringClassName().equals(field.getDeclaringClass().getName()) - && e.getFieldName().equals(field.getName())) + && e.getFieldName().equals(field.getName())) .findFirst(); if (setter.isPresent()) { visitSetter(mv, setter.get(), value, varIndexEntity); @@ -195,7 +179,7 @@ public abstract class ByteCodeHelper { } //use reflection - visitSetViaReflection(mv, field, value, varIndexEntity, varIndexField); + visitSetViaReflection(mv, field, value, varIndexEntity, className, defineFieldCallback); } //endregion @@ -383,6 +367,17 @@ public abstract class ByteCodeHelper { mv.visitInsn(Opcodes.ATHROW); } + public static void visitPushDefault(MethodVisitor mv, Class type) { + if (Set.of(void.class, boolean.class, byte.class, char.class, short.class, int.class, float.class).contains(type)) { + mv.visitInsn(Opcodes.ICONST_0); + } else if (Set.of(double.class, long.class).contains(type)) { + mv.visitInsn(Opcodes.ICONST_0); + mv.visitInsn(Opcodes.ICONST_0); + } else { + mv.visitInsn(Opcodes.ACONST_NULL); + } + } + static String getDescriptorForClassName(String className) { return switch (className) { case "void" -> "V"; @@ -399,6 +394,33 @@ public abstract class ByteCodeHelper { } //endregion + //region field reflection + public static void visitReflectedFieldDefinition(ClassVisitor visitor, Field field, boolean synthetic) { + visitor.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | (synthetic ? Opcodes.ACC_SYNTHETIC : 0), + makeReflectedFieldName(field), T_FIELD.getDescriptor(), null, null); + } + + public static void visitReflectedFieldInit(MethodVisitor mv, Field field, String className) { + className = className.replace(".", "/"); + var fieldname = makeReflectedFieldName(field); + + // field = EntityClass.class.getDeclaredField("fieldname"); + mv.visitLdcInsn(Type.getObjectType(field.getDeclaringClass().getName())); + mv.visitLdcInsn(field.getName()); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_CLASS.getInternalName(), "getDeclaredField", Type.getMethodDescriptor(T_FIELD, T_STRING), false); + mv.visitFieldInsn(Opcodes.PUTSTATIC, className, fieldname, T_FIELD.getDescriptor()); + + // field.setAccessible(true); + mv.visitFieldInsn(Opcodes.GETSTATIC, className, fieldname, Type.getDescriptor(field.getType())); + mv.visitInsn(Opcodes.ICONST_1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setAccessible", Type.getMethodDescriptor(T_VOID, T_BOOL), false); + } + + private static String makeReflectedFieldName(Field field) { + return field.getDeclaringClass().getName().replace(".", "_") + "_" + field.getName(); + } + //endregion + //region var access // private void visitLoadVariable(MethodVisitor mv, String className, int varIndex) { // switch (className) { diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java index 051efce..9b9a243 100644 --- a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java @@ -24,40 +24,53 @@ public abstract class MysqlEntityDbInterface { } protected abstract PreparedStatement queryAdd(Connection connection) throws SQLException; -// protected abstract PreparedStatement queryAdd(Connection connection) throws SQLException { -// return connection.prepareStatement("query", Statement.RETURN_GENERATED_KEYS); -// } - // protected abstract void bindParamsAdd(PreparedStatement stmt, T entity) throws SQLException; - protected void bindParamsAdd(PreparedStatement stmt, T entity) throws SQLException { - stmt.setInt(1, 1); - stmt.setInt(2, 2); - var s = "string"; - if (s != null) { - stmt.setString(3, "string"); - } else { - stmt.setNull(3, Types.INTEGER); - } - stmt.setInt(4, 4); - } + protected abstract void bindParamsAdd(PreparedStatement stmt, T entity) throws SQLException; protected void readBackGeneratedValuesAdd(PreparedStatement stmt, T entity) throws SQLException { try (var res = stmt.getGeneratedKeys()) { readBackGeneratedValuesAdd(res, entity); - } catch (Exception e) { - throw new RuntimeException(e); } } - protected void readBackGeneratedValuesAdd(ResultSet res, T entity) throws Exception { - var f = Exception.class.getDeclaredField("f"); - f.setAccessible(true); - f.setInt(null, 12); + protected abstract void readBackGeneratedValuesAdd(ResultSet res, T entity) throws SQLException; + + public void update(Connection connection, T entity) throws SQLException { + try (var stmt = queryUpdate(connection)) { + bindParamsUpdate(stmt, entity); + + var updated = stmt.executeUpdate(); + if (updated != 1) { + throw new SQLException("Failed to update entity"); + } + + readBackGeneratedValuesUpdate(stmt, entity); + } } -// public void update(Connection connection, T model); -// -// public void delete(Connection connection, T model); -// -// public T read(ResultSet res); + protected abstract PreparedStatement queryUpdate(Connection connection) throws SQLException; + + protected abstract void bindParamsUpdate(PreparedStatement stmt, T entity) throws SQLException; + + protected void readBackGeneratedValuesUpdate(PreparedStatement stmt, T entity) throws SQLException { + try (var res = stmt.getGeneratedKeys()) { + readBackGeneratedValuesUpdate(res, entity); + } + } + + protected abstract void readBackGeneratedValuesUpdate(ResultSet res, T entity) throws SQLException; + + public void delete(Connection connection, T entity) throws SQLException { + try (var stmt = queryUpdate(connection)) { + bindParamsDelete(stmt, entity); + + var deleted = stmt.executeUpdate(); + if (deleted != 1) { + throw new SQLException("Failed to delete entity"); + } + } + } + protected abstract void bindParamsDelete(PreparedStatement stmt, T entity) throws SQLException; + + public abstract T read(ResultSet res); } diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGenerator.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGenerator.java index c1f66e3..2237bb7 100644 --- a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGenerator.java +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGenerator.java @@ -17,8 +17,13 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.lang.reflect.Field; +import java.sql.Statement; +import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -34,6 +39,7 @@ public class MysqlEntityDbInterfaceGenerator { //runtime private ClassDescription classDescription; + private final Set fieldsToReflect = new HashSet<>(); //results @Getter @@ -45,7 +51,7 @@ public class MysqlEntityDbInterfaceGenerator { this.context = context; this.entity = entity; writer = new ClassWriter(Opcodes.ASM9); - visitor = new ClassVisitor(Opcodes.ASM9, writer) {//TODO definitely broken here + visitor = new ClassVisitor(Opcodes.ASM9, writer) { }; } @@ -57,15 +63,30 @@ public class MysqlEntityDbInterfaceGenerator { className = entity.getType().getSimpleName() + "" + MysqlEntityDbInterface.class.getSimpleName(); defineClass(); - defineConstructor(); -// defineAddFunction(); + + //add defineQueryAddFunction(); defineBindParamsAddFunction(); defineReadBackGeneratedValuesAddPreparedStatement(); defineReadBackGeneratedValuesAddResultSet(); -// defineUpdateFunction(); -// defineDeleteFunction(); -// defineReadFunction(); + + //update + defineQueryUpdateFunction(); + defineBindParamsUpdateFunction(); + defineReadBackGeneratedValuesUpdatePreparedStatement(); + defineReadBackGeneratedValuesUpdateResultSet(); + + //delete + defineQueryDeleteFunction(); + defineBindParamsDeleteFunction(); + + //read + defineReadFunction(); + + //other + defineReflectedFields(); + defineClassConstructor(); + defineConstructor(); visitor.visitEnd(); @@ -74,10 +95,7 @@ public class MysqlEntityDbInterfaceGenerator { protected void defineClass() { var classSimpleName = className.substring(Math.max(className.lastIndexOf("."), className.lastIndexOf("$")) + 1); - visitor.visit(Opcodes.V17, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | SYNTHETIC_FLAG, - classSimpleName, - Type.getDescriptor(MysqlEntityDbInterface.class).replace(";", "<" + ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()) + ">;"), - Type.getInternalName(MysqlEntityDbInterface.class), new String[]{}); + visitor.visit(Opcodes.V17, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | SYNTHETIC_FLAG, classSimpleName, Type.getDescriptor(MysqlEntityDbInterface.class).replace(";", "<" + ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()) + ">;"), Type.getInternalName(MysqlEntityDbInterface.class), new String[]{}); // visitor.visitNestHost(Type.getInternalName(context)); // visitor.visitInnerClass(className.replace(".", "/"), Type.getInternalName(context), classSimpleName, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SUPER); } @@ -93,84 +111,39 @@ public class MysqlEntityDbInterfaceGenerator { mv.visitEnd(); } - private ColumnsAndValueBinders prepareColumnsAndValueBindersAdd() { - var columns = new ArrayList(); - var valueBinders = new ArrayList(); - - for (DbField field : entity.getFields()) { - if (!field.isDatabaseField()) { - continue; - } - - Field f = field.getField(); - if (f == null) { - if (field.isDatabaseField()) {//field defined but not present in entity class -> currently nothing to insert -> null - columns.add(field.getName()); - valueBinders.add((mv, varIndexEntity, varIndexStmt, varIndexField, index) -> { - ByteCodeHelper.visitIntInsn(mv, index); - - //stmt.setNull(i, Types.INTEGER); - ByteCodeHelper.visitPreparedStatementSetNull(mv, field, varIndexStmt, index); - }); - } - continue; - } - - columns.add(field.getName()); - valueBinders.add((mv, varIndexEntity, varIndexStmt, varIndexField, index) -> { - if (field.isNotNull()) { - //stmt.set*(i, value); - ByteCodeHelper.visitPreparedStatementSetAny(mv, field, varIndexStmt, index, (mv2) -> ByteCodeHelper.visitGetField(mv2, field.getField(), varIndexEntity, varIndexField, classDescription)); - } else { - // getFieldValue(entity); - ByteCodeHelper.visitGetField(mv, f, varIndexEntity, varIndexField, classDescription); - - // if (value == null) - // stmt.setNull(i, Types.*); - // } else { - // stmt.set*(i, value); - // } - var labelAfter = new Label(); - var labelElse = new Label(); - mv.visitJumpInsn(Opcodes.IFNULL, labelElse); - ByteCodeHelper.visitPreparedStatementSetAny(mv, field, varIndexStmt, index, (mv2) -> ByteCodeHelper.visitGetField(mv2, field.getField(), varIndexEntity, varIndexField, classDescription)); - mv.visitJumpInsn(Opcodes.GOTO, labelAfter); - mv.visitLabel(labelElse); - ByteCodeHelper.visitPreparedStatementSetNull(mv, field, varIndexStmt, index); - mv.visitLabel(labelAfter); -// mv.visitFrame(Opcodes.F_SAME, 0, new Object[0], 0, new Object[0]); - mv.visitFrame(Opcodes.F_FULL, 3, new Object[]{ - ByteCodeHelper.T_MYSQL_ENTITY_DB_INTERFACE.getInternalName(), ByteCodeHelper.T_PREPARED_STATEMENT.getInternalName(), entity.getTypeName().replace(".", "/") - }, 0, new Object[0]); - } - }); + private void defineReflectedFields() { + for (Field field : fieldsToReflect) { + ByteCodeHelper.visitReflectedFieldDefinition(visitor, field, SET_SYNTHETIC_FLAG); } - return new ColumnsAndValueBinders(columns, valueBinders); } - private interface ValueBinder { - void bind(MethodVisitor mv, int varIndexEntity, int varIndexStmt, int varIndexField, int index); - } - - @AllArgsConstructor - @Getter - private static class ColumnsAndValueBinders { - private final List columns; - private final List valueBinders; + // + private void defineClassConstructor() { + var mv = visitor.visitMethod(Opcodes.ACC_STATIC | SYNTHETIC_FLAG, "", "()V", null, new String[0]); + mv.visitCode(); + + //init reflected fields + for (Field field : fieldsToReflect) { + ByteCodeHelper.visitReflectedFieldInit(mv, field, className); + } + + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(2, 0); + mv.visitEnd(); } + //region add private void defineQueryAddFunction() { - var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "queryAdd", Type.getMethodDescriptor(ByteCodeHelper.T_PREPARED_STATEMENT, ByteCodeHelper.T_CONNECTION), - null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "queryAdd", Type.getMethodDescriptor(ByteCodeHelper.T_PREPARED_STATEMENT, ByteCodeHelper.T_CONNECTION), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); var varIndexThis = 0; var varIndexConnection = 1; - var columnsAndValueBinders = prepareColumnsAndValueBindersAdd(); + var columnsAndValueBindersSet = prepareColumnsAndValueBindersSet(); - var columnsString = columnsAndValueBinders.getColumns().stream().map(e -> "`" + e + "`").collect(Collectors.joining(",")); - var placeholdersString = columnsAndValueBinders.getColumns().stream().map(e -> "?").collect(Collectors.joining(",")); + var columnsString = columnsAndValueBindersSet.getColumns().stream().map(e -> "`" + e + "`").collect(Collectors.joining(",")); + var placeholdersString = columnsAndValueBindersSet.getColumns().stream().map(e -> "?").collect(Collectors.joining(",")); var query = "INSERT INTO `" + entity.getName() + "` (" + columnsString + ") VALUES (" + placeholdersString + ")"; // variable scopes @@ -189,8 +162,8 @@ public class MysqlEntityDbInterfaceGenerator { //return connection.prepareStatement("query", Statement.RETURN_GENERATED_KEYS); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn(query); - if (entity.getPrimaryKey() != null) { - mv.visitInsn(Opcodes.ICONST_1); //Statement.RETURN_GENERATED_KEYS == 1 + if (entity.getFields().stream().anyMatch(e -> e.getGenerated() != Generated.Type.NONE)) { + ByteCodeHelper.visitIntInsn(mv, Statement.RETURN_GENERATED_KEYS); } else { mv.visitInsn(Opcodes.ICONST_0); //no flags } @@ -211,17 +184,15 @@ public class MysqlEntityDbInterfaceGenerator { } private void defineBindParamsAddFunction() { - var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "bindParamsAdd", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), - null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "bindParamsAdd", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); var varIndexThis = 0; var varIndexStmt = 1; var varIndexEntity = 2; - var varIndexField = 3; //create lists for columns and value binders - var columnsAndValueBinders = prepareColumnsAndValueBindersAdd(); + var columnsAndValueBindersSet = prepareColumnsAndValueBindersSet(); // variable scopes var labelStartThis = new Label(); @@ -230,8 +201,6 @@ public class MysqlEntityDbInterfaceGenerator { var labelEndStmt = new Label(); var labelStartEntity = new Label(); var labelEndEntity = new Label(); - var labelStartField = new Label(); - var labelEndField = new Label(); // begin mv.visitCode(); @@ -240,12 +209,11 @@ public class MysqlEntityDbInterfaceGenerator { mv.visitLabel(labelStartThis); mv.visitLabel(labelStartStmt); mv.visitLabel(labelStartEntity); - mv.visitLabel(labelStartField); //stmt.set* bind calls var i = 1; - for (var valueBinder : columnsAndValueBinders.getValueBinders()) { - valueBinder.bind(mv, varIndexEntity, varIndexStmt, varIndexField, i++); + for (var valueBinder : columnsAndValueBindersSet.getValueBinders()) { + valueBinder.bind(mv, varIndexEntity, varIndexStmt, i++); } //return @@ -255,55 +223,503 @@ public class MysqlEntityDbInterfaceGenerator { mv.visitLabel(labelEndThis); mv.visitLabel(labelEndStmt); mv.visitLabel(labelEndEntity); - mv.visitLabel(labelEndField); // debug symbols mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartThis, labelEndThis, varIndexThis); mv.visitLocalVariable("stmt", ByteCodeHelper.T_PREPARED_STATEMENT.getDescriptor(), null, labelStartStmt, labelEndStmt, varIndexStmt); mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, labelStartEntity, labelEndEntity, varIndexEntity); - mv.visitLocalVariable("field", ByteCodeHelper.T_FIELD.getDescriptor(), null, labelStartField, labelEndField, varIndexField); - mv.visitMaxs(3, 4); + mv.visitMaxs(3, 3); mv.visitEnd(); } private void defineReadBackGeneratedValuesAddPreparedStatement() { - if (entity.getFields().stream().anyMatch(e -> e.getGenerated() != Generated.Type.NONE)) { + if (entity.getFields().stream().anyMatch(e -> e.getGenerated() == Generated.Type.IDENTITY)) { return; } //stub method if no generated values - var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "readBackGeneratedValuesAdd", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), - null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "readBackGeneratedValuesAdd", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); - var label = new Label(); + var labelStart = new Label(); + var labelEnd = new Label(); + + mv.visitLabel(labelStart); mv.visitCode(); + mv.visitInsn(Opcodes.RETURN); + + mv.visitLabel(labelEnd); // debug symbols - mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, label, label, 0); - mv.visitLocalVariable("stmt", ByteCodeHelper.T_PREPARED_STATEMENT.getDescriptor(), null, label, label, 1); - mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, label, label, 2); + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStart, labelEnd, 0); + mv.visitLocalVariable("stmt", ByteCodeHelper.T_PREPARED_STATEMENT.getDescriptor(), null, labelStart, labelEnd, 1); + mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, labelStart, labelEnd, 2); - mv.visitMaxs(0, 3);//TODO stack size + mv.visitMaxs(0, 3); mv.visitEnd(); } private void defineReadBackGeneratedValuesAddResultSet() { - if (entity.getFields().stream().noneMatch(e -> e.getGenerated() != Generated.Type.NONE)) { + var fieldsToRead = entity.getFields().stream().filter(e -> e.getGenerated() != Generated.Type.NONE).toList(); + if (fieldsToRead.isEmpty()) { return; } - var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "readBackGeneratedValuesAdd", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_RESULT_SET, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), - null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "readBackGeneratedValuesAdd", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_RESULT_SET, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + visitReadFieldsIntoEntityFunctionBody(mv, fieldsToRead, true); + } + //endregion + + //region update + private void defineQueryUpdateFunction() { + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "queryUpdate", Type.getMethodDescriptor(ByteCodeHelper.T_PREPARED_STATEMENT, ByteCodeHelper.T_CONNECTION), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var varIndexThis = 0; + var varIndexConnection = 1; + + var columnsAndValueBindersSet = prepareColumnsAndValueBindersSet(); + var columnsAndValueBindersWhere = prepareColumnsAndValueBindersWhere(); + + var updatesString = columnsAndValueBindersSet.getColumns().stream().map(e -> "`" + e + "` = ?").collect(Collectors.joining(", ")); + var whereString = columnsAndValueBindersWhere.getColumns().stream().map(e -> "`" + e + "` = ?").collect(Collectors.joining(" AND ")); + + var query = "UPDATE `" + entity.getName() + "` SET " + updatesString + " WHERE " + whereString; + + // variable scopes + var labelStartThis = new Label(); + var labelEndThis = new Label(); + var labelStartConnection = new Label(); + var labelEndConnection = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + mv.visitLabel(labelStartThis); + mv.visitLabel(labelStartConnection); + + //return connection.prepareStatement("query", Statement.RETURN_GENERATED_KEYS); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitLdcInsn(query); + if (entity.getFields().stream().anyMatch(e -> e.getGenerated() == Generated.Type.COMPUTED)) { + ByteCodeHelper.visitIntInsn(mv, Statement.RETURN_GENERATED_KEYS); + } else { + mv.visitInsn(Opcodes.ICONST_0); //no flags + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, ByteCodeHelper.T_CONNECTION.getInternalName(), "prepareStatement", Type.getMethodDescriptor(ByteCodeHelper.T_PREPARED_STATEMENT, ByteCodeHelper.T_STRING, ByteCodeHelper.T_INT), true); + mv.visitInsn(Opcodes.ARETURN); + + // variable scopes + mv.visitLabel(labelEndConnection); + mv.visitLabel(labelEndThis); + + // debug symbols + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartConnection, labelEndConnection, varIndexThis); + mv.visitLocalVariable("connection", ByteCodeHelper.T_CONNECTION.getDescriptor(), null, labelStartThis, labelEndThis, varIndexConnection); + + mv.visitMaxs(3, 2); + + mv.visitEnd(); + } + + private void defineBindParamsUpdateFunction() { + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "bindParamsUpdate", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var varIndexThis = 0; + var varIndexStmt = 1; + var varIndexEntity = 2; + + //create lists for columns and value binders + var columnsAndValueBindersSet = prepareColumnsAndValueBindersSet(); + var columnsAndValueBindersWhere = prepareColumnsAndValueBindersWhere(); + + // variable scopes + var labelStartThis = new Label(); + var labelEndThis = new Label(); + var labelStartStmt = new Label(); + var labelEndStmt = new Label(); + var labelStartEntity = new Label(); + var labelEndEntity = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + mv.visitLabel(labelStartThis); + mv.visitLabel(labelStartStmt); + mv.visitLabel(labelStartEntity); + + //stmt.set* bind calls + var i = 1; + for (var valueBinder : columnsAndValueBindersSet.getValueBinders()) { + valueBinder.bind(mv, varIndexEntity, varIndexStmt, i++); + } + for (var valueBinder : columnsAndValueBindersWhere.getValueBinders()) { + valueBinder.bind(mv, varIndexEntity, varIndexStmt, i++); + } + + //return + mv.visitInsn(Opcodes.RETURN); + + // variable scopes + mv.visitLabel(labelEndThis); + mv.visitLabel(labelEndStmt); + mv.visitLabel(labelEndEntity); + + // debug symbols + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartThis, labelEndThis, varIndexThis); + mv.visitLocalVariable("stmt", ByteCodeHelper.T_PREPARED_STATEMENT.getDescriptor(), null, labelStartStmt, labelEndStmt, varIndexStmt); + mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, labelStartEntity, labelEndEntity, varIndexEntity); + + mv.visitMaxs(3, 3); + + mv.visitEnd(); + } + + private void defineReadBackGeneratedValuesUpdatePreparedStatement() { + if (entity.getFields().stream().anyMatch(e -> e.getGenerated() == Generated.Type.COMPUTED)) { + return; + } + + //stub method if no generated values + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "readBackGeneratedValuesUpdate", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var labelStart = new Label(); + var labelEnd = new Label(); + + mv.visitLabel(labelStart); + + mv.visitCode(); + mv.visitInsn(Opcodes.RETURN); + + mv.visitLabel(labelEnd); + + // debug symbols + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStart, labelEnd, 0); + mv.visitLocalVariable("stmt", ByteCodeHelper.T_PREPARED_STATEMENT.getDescriptor(), null, labelStart, labelEnd, 1); + mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, labelStart, labelEnd, 2); + + mv.visitMaxs(0, 3); + mv.visitEnd(); + } + + private void defineReadBackGeneratedValuesUpdateResultSet() { + var fieldsToRead = entity.getFields().stream().filter(e -> e.getGenerated() == Generated.Type.COMPUTED).toList(); + if (fieldsToRead.isEmpty()) { + return; + } + + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "readBackGeneratedValuesUpdate", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_RESULT_SET, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + visitReadFieldsIntoEntityFunctionBody(mv, fieldsToRead, true); + } + //endregion + + //region delete + private void defineQueryDeleteFunction() { + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "queryDelete", Type.getMethodDescriptor(ByteCodeHelper.T_PREPARED_STATEMENT, ByteCodeHelper.T_CONNECTION), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var varIndexThis = 0; + var varIndexConnection = 1; + + var columnsAndValueBindersWhere = prepareColumnsAndValueBindersWhere(); + + var whereString = columnsAndValueBindersWhere.getColumns().stream().map(e -> "`" + e + "` = ?").collect(Collectors.joining(" AND ")); + + var query = "DELETE FROM `" + entity.getName() + "` WHERE " + whereString; + + // variable scopes + var labelStartThis = new Label(); + var labelEndThis = new Label(); + var labelStartConnection = new Label(); + var labelEndConnection = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + mv.visitLabel(labelStartThis); + mv.visitLabel(labelStartConnection); + + //return connection.prepareStatement("query", Statement.RETURN_GENERATED_KEYS); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitLdcInsn(query); + if (entity.getFields().stream().anyMatch(e -> e.getGenerated() != Generated.Type.NONE)) { + ByteCodeHelper.visitIntInsn(mv, Statement.RETURN_GENERATED_KEYS); + } else { + mv.visitInsn(Opcodes.ICONST_0); //no flags + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, ByteCodeHelper.T_CONNECTION.getInternalName(), "prepareStatement", Type.getMethodDescriptor(ByteCodeHelper.T_PREPARED_STATEMENT, ByteCodeHelper.T_STRING, ByteCodeHelper.T_INT), true); + mv.visitInsn(Opcodes.ARETURN); + + // variable scopes + mv.visitLabel(labelEndConnection); + mv.visitLabel(labelEndThis); + + // debug symbols + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartConnection, labelEndConnection, varIndexThis); + mv.visitLocalVariable("connection", ByteCodeHelper.T_CONNECTION.getDescriptor(), null, labelStartThis, labelEndThis, varIndexConnection); + + mv.visitMaxs(3, 2); + + mv.visitEnd(); + } + + private void defineBindParamsDeleteFunction() { + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "bindParamsDelete", Type.getMethodDescriptor(ByteCodeHelper.T_VOID, ByteCodeHelper.T_PREPARED_STATEMENT, Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()))), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var varIndexThis = 0; + var varIndexStmt = 1; + var varIndexEntity = 2; + + //create lists for columns and value binders + var columnsAndValueBindersWhere = prepareColumnsAndValueBindersWhere(); + + // variable scopes + var labelStartThis = new Label(); + var labelEndThis = new Label(); + var labelStartStmt = new Label(); + var labelEndStmt = new Label(); + var labelStartEntity = new Label(); + var labelEndEntity = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + mv.visitLabel(labelStartThis); + mv.visitLabel(labelStartStmt); + mv.visitLabel(labelStartEntity); + + //stmt.set* bind calls + var i = 1; + for (var valueBinder : columnsAndValueBindersWhere.getValueBinders()) { + valueBinder.bind(mv, varIndexEntity, varIndexStmt, i++); + } + + //return + mv.visitInsn(Opcodes.RETURN); + + // variable scopes + mv.visitLabel(labelEndThis); + mv.visitLabel(labelEndStmt); + mv.visitLabel(labelEndEntity); + + // debug symbols + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartThis, labelEndThis, varIndexThis); + mv.visitLocalVariable("stmt", ByteCodeHelper.T_PREPARED_STATEMENT.getDescriptor(), null, labelStartStmt, labelEndStmt, varIndexStmt); + mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, labelStartEntity, labelEndEntity, varIndexEntity); + + mv.visitMaxs(3, 3); + + mv.visitEnd(); + } + //endregion + + //region read + private void defineReadFunction() { + var fields = entity.getFields().stream().filter(e -> e.isDatabaseField()).toList(); + var bestConstructor = classDescription.getDeclaredConstructors().stream().filter(e -> e.getSuperConstructorParameterDescriptions().size() == 0).map(ctor -> { + var matchingParamCount = ctor.getConstructorParameterDescriptions().stream() + .filter(param -> fields.stream().anyMatch(field -> field.getField().getDeclaringClass().getName().equals(param.getDeclaringClassName()) + && field.getField().getName().equals(param.getFieldName()))) + .count(); + return new AbstractMap.SimpleEntry<>(ctor, matchingParamCount); + }).sorted(Comparator.comparingLong(e -> -e.getValue())).findFirst(); + if (!bestConstructor.isPresent()) { + throw new IllegalStateException("No suitable constructor found!"); + } + + var orderedFields = bestConstructor.orElseThrow().getKey().getConstructorParameterDescriptions().stream() + .sorted(Comparator.comparingInt(e -> e.getParameterIndex())) + .map(param -> new AbstractMap.SimpleEntry<>(param, fields.stream() + .filter(field -> field.getField().getDeclaringClass().getName().equals(param.getDeclaringClassName()) + && field.getField().getName().equals(param.getFieldName())) + .findFirst().orElse(null))) + .toList(); + + //begin + var mv = visitor.visitMethod(Opcodes.ACC_PROTECTED | SYNTHETIC_FLAG, "read", Type.getMethodDescriptor(Type.getType(ByteCodeHelper.getDescriptorForClassName(entity.getTypeName())), ByteCodeHelper.T_RESULT_SET), null, new String[]{ByteCodeHelper.T_SQLEXCEPTION.getInternalName()}); mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + var varIndexThis = 0; + var varIndexRes = 1; + + // variable scopes + var labelStartThis = new Label(); + var labelEndThis = new Label(); + var labelStartRes = new Label(); + var labelEndRes = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + mv.visitLabel(labelStartThis); + mv.visitLabel(labelStartRes); + + // push params + mv.visitLdcInsn(Type.getObjectType(entity.getTypeName().replace(".", "/"))); + for (var paramAndField : orderedFields) { + if (paramAndField.getValue() == null) { + var fieldType = entity.getFields().stream() + .filter(field -> field.getField().getDeclaringClass().getName().equals(paramAndField.getKey().getDeclaringClassName()) + && field.getField().getName().equals(paramAndField.getKey().getFieldName())) + .findFirst().orElseThrow() + .getField().getType(); + // PUSH null, 0, or false + ByteCodeHelper.visitPushDefault(mv, fieldType); + } else { + // PUSH res.get*(1); + ByteCodeHelper.visitResultSetGetAny(mv, paramAndField.getValue(), varIndexRes); + } + } + + //new Entity(...) + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, entity.getTypeName().replace(".", "/"), "", + Type.getMethodDescriptor(ByteCodeHelper.T_VOID, orderedFields.stream() + .map(paramAndField -> Type.getType(ByteCodeHelper.getDescriptorForClassName(paramAndField.getKey().getParameterClassName()))) + .toArray(Type[]::new)), false); + + //return + mv.visitInsn(Opcodes.ARETURN); + + // variable scopes + mv.visitLabel(labelEndThis); + mv.visitLabel(labelEndRes); + + // debug symbols + mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartThis, labelEndThis, varIndexThis); + mv.visitLocalVariable("res", ByteCodeHelper.T_RESULT_SET.getDescriptor(), null, labelStartRes, labelEndRes, varIndexRes); + + mv.visitMaxs(orderedFields.size() + 1, 2); + + mv.visitEnd(); + } + //endregion + + //region util + private ColumnsAndValueBinders prepareColumnsAndValueBindersSet() { + var columns = new ArrayList(); + var valueBinders = new ArrayList(); + + for (DbField field : entity.getFields()) { + if (!field.isDatabaseField()) { + continue; + } + + Field f = field.getField(); + if (f == null) { + if (field.isDatabaseField()) {//field defined but not present in entity class -> currently nothing to insert -> null + columns.add(field.getName()); + valueBinders.add((mv, varIndexEntity, varIndexStmt, index) -> { + ByteCodeHelper.visitIntInsn(mv, index); + + //stmt.setNull(i, Types.INTEGER); + ByteCodeHelper.visitPreparedStatementSetNull(mv, field, varIndexStmt, index); + }); + } + continue; + } + + columns.add(field.getName()); + valueBinders.add((mv, varIndexEntity, varIndexStmt, index) -> { + if (field.isNotNull()) { + //stmt.set*(i, value); + ByteCodeHelper.visitPreparedStatementSetAny(mv, field, varIndexStmt, index, (mv2) -> ByteCodeHelper.visitGetField(mv2, field.getField(), varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription)); + } else { + // getFieldValue(entity); + ByteCodeHelper.visitGetField(mv, f, varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription); + + // if (value == null) + // stmt.setNull(i, Types.*); + // } else { + // stmt.set*(i, value); + // } + var labelAfter = new Label(); + var labelElse = new Label(); + mv.visitJumpInsn(Opcodes.IFNULL, labelElse); + ByteCodeHelper.visitPreparedStatementSetAny(mv, field, varIndexStmt, index, (mv2) -> ByteCodeHelper.visitGetField(mv2, field.getField(), varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription)); + mv.visitJumpInsn(Opcodes.GOTO, labelAfter); + mv.visitLabel(labelElse); + ByteCodeHelper.visitPreparedStatementSetNull(mv, field, varIndexStmt, index); + mv.visitLabel(labelAfter); +// mv.visitFrame(Opcodes.F_SAME, 0, new Object[0], 0, new Object[0]); + mv.visitFrame(Opcodes.F_FULL, 3, new Object[]{ByteCodeHelper.T_MYSQL_ENTITY_DB_INTERFACE.getInternalName(), ByteCodeHelper.T_PREPARED_STATEMENT.getInternalName(), entity.getTypeName().replace(".", "/")}, 0, new Object[0]); + } + }); + } + return new ColumnsAndValueBinders(columns, valueBinders); + } + + private ColumnsAndValueBinders prepareColumnsAndValueBindersWhere() { + var columns = new ArrayList(); + var valueBinders = new ArrayList(); + + for (DbField field : entity.getPrimaryKey().getFields()) { + if (!field.isDatabaseField()) { + continue; + } + + Field f = field.getField(); + if (f == null) { + if (field.isDatabaseField()) {//field defined but not present in entity class -> currently nothing to insert -> null + columns.add(field.getName()); + valueBinders.add((mv, varIndexEntity, varIndexStmt, index) -> { + ByteCodeHelper.visitIntInsn(mv, index); + + //stmt.setNull(i, Types.INTEGER); + ByteCodeHelper.visitPreparedStatementSetNull(mv, field, varIndexStmt, index); + }); + } + continue; + } + + columns.add(field.getName()); + valueBinders.add((mv, varIndexEntity, varIndexStmt, index) -> { + if (field.isNotNull()) { + //stmt.set*(i, value); + ByteCodeHelper.visitPreparedStatementSetAny(mv, field, varIndexStmt, index, (mv2) -> ByteCodeHelper.visitGetField(mv2, field.getField(), varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription)); + } else { + // getFieldValue(entity); + ByteCodeHelper.visitGetField(mv, f, varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription); + + // if (value == null) + // stmt.setNull(i, Types.*); + // } else { + // stmt.set*(i, value); + // } + var labelAfter = new Label(); + var labelElse = new Label(); + mv.visitJumpInsn(Opcodes.IFNULL, labelElse); + ByteCodeHelper.visitPreparedStatementSetAny(mv, field, varIndexStmt, index, (mv2) -> ByteCodeHelper.visitGetField(mv2, field.getField(), varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription)); + mv.visitJumpInsn(Opcodes.GOTO, labelAfter); + mv.visitLabel(labelElse); + ByteCodeHelper.visitPreparedStatementSetNull(mv, field, varIndexStmt, index); + mv.visitLabel(labelAfter); +// mv.visitFrame(Opcodes.F_SAME, 0, new Object[0], 0, new Object[0]); + mv.visitFrame(Opcodes.F_FULL, 3, new Object[]{ByteCodeHelper.T_MYSQL_ENTITY_DB_INTERFACE.getInternalName(), ByteCodeHelper.T_PREPARED_STATEMENT.getInternalName(), entity.getTypeName().replace(".", "/")}, 0, new Object[0]); + } + }); + } + return new ColumnsAndValueBinders(columns, valueBinders); + } + + private void visitReadFieldsIntoEntityFunctionBody(MethodVisitor mv, List> fields, boolean moveNext) { var varIndexThis = 0; var varIndexRes = 1; var varIndexEntity = 2; - var varIndexField = 3; // variable scopes var labelStartThis = new Label(); @@ -312,8 +728,6 @@ public class MysqlEntityDbInterfaceGenerator { var labelEndRes = new Label(); var labelStartEntity = new Label(); var labelEndEntity = new Label(); - var labelStartField = new Label(); - var labelEndField = new Label(); // begin mv.visitCode(); @@ -322,30 +736,27 @@ public class MysqlEntityDbInterfaceGenerator { mv.visitLabel(labelStartThis); mv.visitLabel(labelStartRes); mv.visitLabel(labelStartEntity); - mv.visitLabel(labelStartField); - // if (!res.next()) { - // throw new SQLException("Missing generated keys for entity but are required"); - // } - var labelResHasNext = new Label(); - mv.visitVarInsn(Opcodes.ALOAD, varIndexRes); - mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, ByteCodeHelper.T_RESULT_SET.getInternalName(), "next", Type.getMethodDescriptor(ByteCodeHelper.T_BOOL), true); - mv.visitJumpInsn(Opcodes.IFNE, labelResHasNext); - ByteCodeHelper.visitThrowSQLException(mv, "Missing generated keys for entity but are required"); - mv.visitLabel(labelResHasNext); - - for (var field : entity.getFields()) { - if (field.getGenerated() == Generated.Type.NONE) { - continue; - } + if (moveNext) { + // if (!res.next()) { + // throw new SQLException("Missing generated keys for entity but are required"); + // } + var labelResHasNext = new Label(); + mv.visitVarInsn(Opcodes.ALOAD, varIndexRes); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, ByteCodeHelper.T_RESULT_SET.getInternalName(), "next", Type.getMethodDescriptor(ByteCodeHelper.T_BOOL), true); + mv.visitJumpInsn(Opcodes.IFNE, labelResHasNext); + ByteCodeHelper.visitThrowSQLException(mv, "Missing generated keys for entity but are required"); + mv.visitLabel(labelResHasNext); + } + for (var field : fields) { // var value = res.get*(1); Consumer resGetValue = (mv2) -> { ByteCodeHelper.visitResultSetGetAny(mv2, field, varIndexRes); }; // *SET*(entity, field, value); - ByteCodeHelper.visitSetField(mv, field.getField(), resGetValue, varIndexEntity, varIndexField, classDescription); + ByteCodeHelper.visitSetField(mv, field.getField(), resGetValue, varIndexEntity, className, () -> fieldsToReflect.add(field.getField()), classDescription); } //return @@ -355,26 +766,26 @@ public class MysqlEntityDbInterfaceGenerator { mv.visitLabel(labelEndThis); mv.visitLabel(labelEndRes); mv.visitLabel(labelEndEntity); - mv.visitLabel(labelEndField); // debug symbols mv.visitLocalVariable("this", ByteCodeHelper.getDescriptorForClassName(className), null, labelStartThis, labelEndThis, varIndexThis); mv.visitLocalVariable("res", ByteCodeHelper.T_RESULT_SET.getDescriptor(), null, labelStartRes, labelEndRes, varIndexRes); mv.visitLocalVariable("entity", ByteCodeHelper.getDescriptorForClassName(entity.getTypeName()), null, labelStartEntity, labelEndEntity, varIndexEntity); - mv.visitLocalVariable("field", ByteCodeHelper.T_FIELD.getDescriptor(), null, labelStartField, labelEndField, varIndexField); - mv.visitMaxs(3, 4); + mv.visitMaxs(3, 3); mv.visitEnd(); } - private void defineUpdateFunction() { + private interface ValueBinder { + void bind(MethodVisitor mv, int varIndexEntity, int varIndexStmt, int index); } - private void defineDeleteFunction() { + @AllArgsConstructor + @Getter + private static class ColumnsAndValueBinders { + private final List columns; + private final List valueBinders; } - - private void defineReadFunction() { - } - + //endregion }