From b805eed62245219844464b2f1ba7d2402f7f0891 Mon Sep 17 00:00:00 2001 From: wea_ondara Date: Tue, 20 Dec 2022 17:33:29 +0100 Subject: [PATCH] wip --- core/src/main/java/jef/DbSet.java | 24 +- core/src/main/java/jef/Queryable.java | 11 +- core/src/main/java/jef/QueryableProxy.java | 9 +- .../java/jef/asm/access/ClassAnalyzer.java | 26 ++ .../jef/asm/access/ClassAnalyzerVisitor.java | 69 +++ .../jef/asm/access/MethodAnalyzerVisitor.java | 308 +++++++++++++ .../asm/access/model/ClassDescription.java | 41 ++ .../access/model/ConstructorDescription.java | 15 + .../ConstructorParameterDescription.java | 14 + .../asm/access/model/FieldDescription.java | 13 + .../asm/access/model/GetterDescription.java | 17 + .../asm/access/model/SetterDescription.java | 17 + .../SuperConstructorParameterDescription.java | 13 + core/src/main/java/jef/model/DbContext.java | 2 +- .../main/java/jef/operations/FilterOp.java | 10 +- .../src/main/java/jef/operations/LimitOp.java | 10 +- .../main/java/jef/operations/Operation.java | 5 +- core/src/main/java/jef/operations/SortOp.java | 10 +- .../main/java/jef/platform/base/Database.java | 87 +++- .../jef/platform/dummy/DummyDatabase.java | 30 ++ core/src/main/java/jef/util/Util.java | 5 + .../jef/asm/access/ClassAnalyzerTest.java | 58 +++ .../java/jef/operations/FilterOpTest.java | 4 +- .../test/java/jef/operations/LimitOpTest.java | 8 +- .../test/java/jef/operations/SortOpTest.java | 8 +- .../visitors/DebugExpressionVisitorTest.java | 4 +- .../jef/platform/mysql/MysqlDatabase.java | 313 +++++++++++++ .../mysql/dbinterface/ByteCodeHelper.java | 423 ++++++++++++++++++ .../dbinterface/MysqlEntityDbInterface.java | 63 +++ .../MysqlEntityDbInterfaceGenerator.java | 380 ++++++++++++++++ .../MysqlInterfaceClassLoader.java | 66 +++ .../dbinterface/MysqlInterfaceFactory.java | 33 ++ .../mysql/MysqlSimpleIntegrativeTest.java | 172 +++++++ .../MysqlEntityDbInterfaceGeneratorTest.java | 86 ++++ 34 files changed, 2319 insertions(+), 35 deletions(-) create mode 100644 core/src/main/java/jef/asm/access/ClassAnalyzer.java create mode 100644 core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java create mode 100644 core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java create mode 100644 core/src/main/java/jef/asm/access/model/ClassDescription.java create mode 100644 core/src/main/java/jef/asm/access/model/ConstructorDescription.java create mode 100644 core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java create mode 100644 core/src/main/java/jef/asm/access/model/FieldDescription.java create mode 100644 core/src/main/java/jef/asm/access/model/GetterDescription.java create mode 100644 core/src/main/java/jef/asm/access/model/SetterDescription.java create mode 100644 core/src/main/java/jef/asm/access/model/SuperConstructorParameterDescription.java create mode 100644 core/src/test/java/jef/asm/access/ClassAnalyzerTest.java create mode 100644 mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java create mode 100644 mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java create mode 100644 mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGenerator.java create mode 100644 mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceClassLoader.java create mode 100644 mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceFactory.java create mode 100644 mysql/src/test/java/jef/platform/mysql/MysqlSimpleIntegrativeTest.java create mode 100644 mysql/src/test/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGeneratorTest.java diff --git a/core/src/main/java/jef/DbSet.java b/core/src/main/java/jef/DbSet.java index d19797d..8199506 100644 --- a/core/src/main/java/jef/DbSet.java +++ b/core/src/main/java/jef/DbSet.java @@ -4,19 +4,24 @@ import jef.expressions.Expression; import jef.expressions.SelectExpression; import jef.expressions.TableExpression; import jef.expressions.selectable.DatabaseSelectAllExpression; +import jef.model.DbContext; import jef.serializable.SerializableObject; import lombok.Getter; +import java.sql.SQLException; import java.util.Iterator; import java.util.List; import java.util.Spliterator; public class DbSet implements Queryable { + @Getter + private final DbContext context; @Getter private final Class clazz; private final String table; - public DbSet(Class clazz, String table) { + public DbSet(DbContext context, Class clazz, String table) { + this.context = context; this.clazz = clazz; this.table = table; } @@ -36,6 +41,23 @@ public class DbSet implements Queryable { return "SELECT * FROM `" + table + "`"; } + @Override + public DbSet originalSet() { + return this; + } + + public void add(T entity) throws SQLException { + context.getDatabase().add(context, clazz, entity); + } + + public void update(T entity) throws SQLException { + context.getDatabase().update(context, clazz, entity); + } + + public void delete(T entity) throws SQLException { + context.getDatabase().delete(context, clazz, entity); + } + @Override public Iterator iterator() { return null; diff --git a/core/src/main/java/jef/Queryable.java b/core/src/main/java/jef/Queryable.java index 658f04a..297c1f5 100644 --- a/core/src/main/java/jef/Queryable.java +++ b/core/src/main/java/jef/Queryable.java @@ -6,13 +6,15 @@ import jef.operations.FilterOp; import jef.operations.LimitOp; import jef.operations.SortOp; import jef.serializable.SerializableFunction; +import jef.serializable.SerializableObject; import jef.serializable.SerializablePredicate; -import java.io.Serializable; +import java.sql.SQLException; import java.util.Iterator; +import java.util.List; import java.util.Spliterator; -public interface Queryable { +public interface Queryable { //TODO documentation //TODO table alias thing still not ready @@ -22,6 +24,8 @@ public interface Queryable { String toString(); + DbSet originalSet(); + //stream functions default Iterator iterator() { return null; @@ -131,6 +135,9 @@ public interface Queryable { // default Object[] toArray() { // return new Object[0]; // } + default List toList() throws SQLException { + return originalSet().getContext().getDatabase().queryExpression(originalSet().getContext(), originalSet().getClazz(), getExpression()); + } // // default A[] toArray(IntFunction intFunction) { // return null; diff --git a/core/src/main/java/jef/QueryableProxy.java b/core/src/main/java/jef/QueryableProxy.java index 0331529..b52406d 100644 --- a/core/src/main/java/jef/QueryableProxy.java +++ b/core/src/main/java/jef/QueryableProxy.java @@ -1,13 +1,13 @@ package jef; import jef.expressions.Expression; +import jef.serializable.SerializableObject; import jef.serializable.SerializablePredicate; -import java.io.Serializable; import java.util.Iterator; import java.util.Spliterator; -public class QueryableProxy implements Queryable { +public class QueryableProxy implements Queryable { private final Queryable delegate; public QueryableProxy(Queryable delegate) { @@ -24,6 +24,11 @@ public class QueryableProxy implements Queryable { return delegate.getExpression(); } + @Override + public DbSet originalSet() { + return delegate.originalSet(); + } + @Override public Iterator iterator() { return delegate.iterator(); diff --git a/core/src/main/java/jef/asm/access/ClassAnalyzer.java b/core/src/main/java/jef/asm/access/ClassAnalyzer.java new file mode 100644 index 0000000..110a336 --- /dev/null +++ b/core/src/main/java/jef/asm/access/ClassAnalyzer.java @@ -0,0 +1,26 @@ +package jef.asm.access; + +import jef.asm.access.model.ClassDescription; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; + +public class ClassAnalyzer { + public ClassDescription analyze(Class clazz) { + return analyzeClass(clazz); + } + + private ClassDescription analyzeClass(Class clazz) { + try (var is = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace(".", "/") + ".class")) { + var result = new ClassDescription[1]; + var byteCode = is.readAllBytes(); + var reader = new ClassReader(byteCode); + var visitor = new ClassAnalyzerVisitor(Opcodes.ASM9, description -> result[0] = description); + reader.accept(visitor, 0); + return result[0]; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java b/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java new file mode 100644 index 0000000..175ac9d --- /dev/null +++ b/core/src/main/java/jef/asm/access/ClassAnalyzerVisitor.java @@ -0,0 +1,69 @@ +package jef.asm.access; + +import jef.asm.access.model.ClassDescription; +import jef.asm.access.model.ConstructorDescription; +import jef.asm.access.model.FieldDescription; +import jef.asm.access.model.GetterDescription; +import jef.asm.access.model.SetterDescription; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +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.function.Consumer; + +class ClassAnalyzerVisitor extends ClassVisitor { + private final Consumer callback; + + private String className; + private String superClassName; + private final List declaredFields = new ArrayList<>(); + private final List declaredConstructor = new ArrayList<>(); + private final List declaredGetters = new ArrayList<>(); + private final List declaredSetters = new ArrayList<>(); + + protected ClassAnalyzerVisitor(int api, Consumer callback) { + super(api); + this.callback = callback; + } + + protected ClassAnalyzerVisitor(int api, ClassVisitor classVisitor, Consumer callback) { + super(api, classVisitor); + this.callback = callback; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + className = name.replace("/", "."); + superClassName = superName.replace("/", "."); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + declaredFields.add(new FieldDescription(className, name)); + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + 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 -> { + declaredConstructor.addAll(entityAccess.getDeclaredConstructors()); + declaredGetters.addAll(entityAccess.getDeclaredGetters()); + declaredSetters.addAll(entityAccess.getDeclaredSetters()); + }); + } + + @Override + public void visitEnd() { + super.visitEnd(); + callback.accept(new ClassDescription(className, superClassName, declaredFields, declaredConstructor, declaredGetters, declaredSetters)); + } +} diff --git a/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java b/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java new file mode 100644 index 0000000..250aba0 --- /dev/null +++ b/core/src/main/java/jef/asm/access/MethodAnalyzerVisitor.java @@ -0,0 +1,308 @@ +package jef.asm.access; + +import jef.asm.access.model.ClassDescription; +import jef.asm.access.model.ConstructorDescription; +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.Getter; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.Stack; +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 methodName; + private final String returnClassName; + private final String[] parameterClassNames; + + private List constructorParameterDescriptions = new ArrayList<>(); + private List superConstructorParameterDescriptions = new ArrayList<>(); + private Stack varIndexStack = new Stack<>(); + private String fieldDeclaringClassName = null; + private String fieldName = null; + + private ConstructorSteps constructorStep = ConstructorSteps.INIT; + private GetterSteps getterStep = GetterSteps.INIT; + private SetterSteps setterStep = SetterSteps.INIT; + + enum ConstructorSteps { + INIT, + CODE, + PRE_SUPER_INIT_ALOAD_0, + PRE_SUPER_INIT_VAR_LOAD, + SUPER_INIT, + ALOAD_0, + VAR_LOAD, + PUT_FIELD, + RETURN, + INVALID + } + + enum GetterSteps { + INIT, + CODE, + ALOAD_0, + GET_FIELD, + RETURN, + INVALID + } + + enum SetterSteps { + INIT, + CODE, + ALOAD_0, + VAR_LOAD, + PUT_FIELD, + RETURN, + INVALID + } + + protected MethodAnalyzerVisitor(int api, String methodClassName, String methodName, String returnClassName, String[] parameterClassNames, Consumer callback) { + super(api); + this.methodClassName = methodClassName; + this.methodName = methodName; + this.returnClassName = returnClassName; + this.parameterClassNames = parameterClassNames; + this.callback = callback; + } + + @Override + public void visitCode() { + constructorStep = constructorStep == ConstructorSteps.INIT ? ConstructorSteps.CODE : ConstructorSteps.INVALID; + getterStep = getterStep == GetterSteps.INIT ? GetterSteps.CODE : GetterSteps.INVALID; + setterStep = setterStep == SetterSteps.INIT ? SetterSteps.CODE : SetterSteps.INVALID; + super.visitCode(); + } + + @Override + public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitFrame(type, numLocal, local, numStack, stack); + } + + @Override + public void visitInsn(int opcode) { + if (Set.of(Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN).contains(opcode)) { + getterStep = getterStep == GetterSteps.GET_FIELD ? GetterSteps.RETURN : GetterSteps.INVALID; + } else { + getterStep = GetterSteps.INVALID; + } + if (opcode == Opcodes.RETURN) { + constructorStep = constructorStep == ConstructorSteps.PUT_FIELD ? ConstructorSteps.RETURN : ConstructorSteps.INVALID; + setterStep = setterStep == SetterSteps.PUT_FIELD ? SetterSteps.RETURN : SetterSteps.INVALID; + } else { + constructorStep = ConstructorSteps.INVALID; + setterStep = SetterSteps.INVALID; + } + super.visitInsn(opcode); + } + + @Override + public void visitIntInsn(int opcode, int operand) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitIntInsn(opcode, operand); + } + + @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); + } + if (opcode == Opcodes.ALOAD && varIndex == 0) { + if (constructorStep == ConstructorSteps.CODE) { + constructorStep = ConstructorSteps.PRE_SUPER_INIT_ALOAD_0; + } else if (constructorStep == ConstructorSteps.SUPER_INIT || constructorStep == ConstructorSteps.PUT_FIELD) { + constructorStep = ConstructorSteps.ALOAD_0; + } else { + constructorStep = ConstructorSteps.INVALID; + } + } else if (Set.of(Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.ALOAD).contains(opcode)) { + if (constructorStep == ConstructorSteps.PRE_SUPER_INIT_ALOAD_0) { + constructorStep = ConstructorSteps.PRE_SUPER_INIT_VAR_LOAD; + } else if (constructorStep == ConstructorSteps.PRE_SUPER_INIT_VAR_LOAD) { + constructorStep = ConstructorSteps.PRE_SUPER_INIT_VAR_LOAD; + } else if (constructorStep == ConstructorSteps.ALOAD_0) { + constructorStep = ConstructorSteps.VAR_LOAD; + } else { + constructorStep = ConstructorSteps.INVALID; + } + } else { + constructorStep = ConstructorSteps.INVALID; + } + if (opcode == Opcodes.ALOAD && varIndex == 0) { + getterStep = getterStep == GetterSteps.CODE ? GetterSteps.ALOAD_0 : GetterSteps.INVALID; + } else { + getterStep = GetterSteps.INVALID; + } + if (opcode == Opcodes.ALOAD && varIndex == 0) { + setterStep = setterStep == SetterSteps.CODE ? SetterSteps.ALOAD_0 : SetterSteps.INVALID; + } else if (Set.of(Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.ALOAD).contains(opcode)) { + setterStep = setterStep == SetterSteps.ALOAD_0 ? SetterSteps.VAR_LOAD : SetterSteps.INVALID; + } else { + setterStep = SetterSteps.INVALID; + } + super.visitVarInsn(opcode, varIndex); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + var lastLoadedVarIndex = -1; + if (opcode == Opcodes.PUTFIELD) { + lastLoadedVarIndex = varIndexStack.pop(); + varIndexStack.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(); + constructorStep = ConstructorSteps.INVALID; + getterStep = getterStep == GetterSteps.ALOAD_0 ? GetterSteps.GET_FIELD : GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + } else { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + } + 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 + super.visitFieldInsn(opcode, owner, name, descriptor); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + var argIndexes = new ArrayList(); + for (var i = 0; i < Type.getArgumentTypes(descriptor).length; i++) { + argIndexes.add(0, varIndexStack.pop()); + } + varIndexStack.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)); + } + constructorStep = ConstructorSteps.SUPER_INIT; + } else { + constructorStep = ConstructorSteps.INVALID; + } + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitLabel(Label label) { +// constructorStep = ConstructorSteps.INVALID; +// getterStep = GetterSteps.INVALID; +// setterStep = SetterSteps.INVALID; + super.visitLabel(label); + } + + @Override + public void visitLdcInsn(Object value) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitLdcInsn(value); + } + + @Override + public void visitIincInsn(int varIndex, int increment) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitIincInsn(varIndex, increment); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override + public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitMultiANewArrayInsn(descriptor, numDimensions); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + constructorStep = ConstructorSteps.INVALID; + getterStep = GetterSteps.INVALID; + setterStep = SetterSteps.INVALID; + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override + public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) { + super.visitLocalVariable(name, descriptor, signature, start, end, index); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + super.visitMaxs(maxStack, maxLocals); + } + + @Override + 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(); + var access = new ClassDescription(null, null, null, constructors, getters, setters); + callback.accept(access); + } +} diff --git a/core/src/main/java/jef/asm/access/model/ClassDescription.java b/core/src/main/java/jef/asm/access/model/ClassDescription.java new file mode 100644 index 0000000..f1fdd42 --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/ClassDescription.java @@ -0,0 +1,41 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +@Getter +public class ClassDescription { + private final String className; + private final String superClassName; + private final List declaredFields; + private final List declaredConstructors; + private final List declaredGetters; + private final List declaredSetters; + + @Override + public String toString() { + return "ClassDescription{" + + "className='" + className + '\'' + + ", superClassName='" + superClassName + '\'' + + ", declaredFields=" + declaredFields + + ", declaredConstructors=" + declaredConstructors + + ", declaredGetters=" + declaredGetters + + ", declaredSetters=" + declaredSetters + + '}'; + } + + public String toStringFriendly() { + return "ClassAccessDescription{\n" + + " className: " + className + '\n' + + " superClassName: " + superClassName + '\n' + + " declaredFields: " + declaredFields + '\n' + + " declaredConstructors:\n" + declaredConstructors.stream().map(e -> " " + e.toString() + '\n').collect(Collectors.joining()) + + " declaredGetters:\n" + declaredGetters.stream().map(e -> " " + e.toString() + '\n').collect(Collectors.joining()) + + " declaredSetters:\n" + declaredSetters.stream().map(e -> " " + e.toString()+ '\n').collect(Collectors.joining()) + + '}'; + } +} diff --git a/core/src/main/java/jef/asm/access/model/ConstructorDescription.java b/core/src/main/java/jef/asm/access/model/ConstructorDescription.java new file mode 100644 index 0000000..5809fd5 --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/ConstructorDescription.java @@ -0,0 +1,15 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@AllArgsConstructor +@Getter +@ToString +public class ConstructorDescription { + private final List constructorParameterDescriptions; + private final List superConstructorParameterDescriptions; +} diff --git a/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java b/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java new file mode 100644 index 0000000..949785c --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/ConstructorParameterDescription.java @@ -0,0 +1,14 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class ConstructorParameterDescription { + private final int parameterIndex; + private final String declaringClassName; + private final String fieldName; +} diff --git a/core/src/main/java/jef/asm/access/model/FieldDescription.java b/core/src/main/java/jef/asm/access/model/FieldDescription.java new file mode 100644 index 0000000..39fea17 --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/FieldDescription.java @@ -0,0 +1,13 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class FieldDescription { + private final String fieldDeclaringClassName; + private final String fieldName; +} diff --git a/core/src/main/java/jef/asm/access/model/GetterDescription.java b/core/src/main/java/jef/asm/access/model/GetterDescription.java new file mode 100644 index 0000000..bbd7b30 --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/GetterDescription.java @@ -0,0 +1,17 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class GetterDescription { + private final String methodDeclaringClassName; + private final String methodName; + private final String methodReturnClassName; + private final String[] methodParameterClassNames; + private final String fieldDeclaringClassName; + private final String fieldName; +} diff --git a/core/src/main/java/jef/asm/access/model/SetterDescription.java b/core/src/main/java/jef/asm/access/model/SetterDescription.java new file mode 100644 index 0000000..2630422 --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/SetterDescription.java @@ -0,0 +1,17 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class SetterDescription { + private final String methodDeclaringClassName; + private final String methodName; + private final String methodReturnClassName; + private final String[] methodParameterClassNames; + private final String fieldDeclaringClassName; + private final String fieldName; +} diff --git a/core/src/main/java/jef/asm/access/model/SuperConstructorParameterDescription.java b/core/src/main/java/jef/asm/access/model/SuperConstructorParameterDescription.java new file mode 100644 index 0000000..43825a0 --- /dev/null +++ b/core/src/main/java/jef/asm/access/model/SuperConstructorParameterDescription.java @@ -0,0 +1,13 @@ +package jef.asm.access.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class SuperConstructorParameterDescription { + private final int constructorParameterIndex; + private final int superConstructorParameterIndex; +} diff --git a/core/src/main/java/jef/model/DbContext.java b/core/src/main/java/jef/model/DbContext.java index 267670d..308f685 100644 --- a/core/src/main/java/jef/model/DbContext.java +++ b/core/src/main/java/jef/model/DbContext.java @@ -40,7 +40,7 @@ public abstract class DbContext { f.setAccessible(true); Clazz anno = f.getAnnotation(Clazz.class); try { - f.set(this, new DbSet(anno.value(), f.getName()));//TODO use table name from modelbuilder + f.set(this, new DbSet(this, anno.value(), f.getName()));//TODO use table name from modelbuilder } catch (IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/core/src/main/java/jef/operations/FilterOp.java b/core/src/main/java/jef/operations/FilterOp.java index 6abc68b..b73a6b9 100644 --- a/core/src/main/java/jef/operations/FilterOp.java +++ b/core/src/main/java/jef/operations/FilterOp.java @@ -1,5 +1,6 @@ package jef.operations; +import jef.DbSet; import jef.Queryable; import jef.asm.AsmParseException; import jef.asm.OptimizedAsmParser; @@ -8,13 +9,13 @@ import jef.expressions.SelectExpression; import jef.expressions.WhereExpression; import jef.expressions.modifier.TableAliasInjector; import jef.expressions.selectable.DatabaseSelectAllExpression; +import jef.serializable.SerializableObject; import jef.serializable.SerializablePredicate; -import java.io.Serializable; import java.util.List; import java.util.function.Predicate; -public class FilterOp implements Queryable, Operation { +public class FilterOp implements Queryable, Operation { private final Queryable queryable; private final Predicate predicate; @@ -50,4 +51,9 @@ public class FilterOp implements Queryable, Operation public String toString() { return getExpression().toString(); } + + @Override + public DbSet originalSet() { + return queryable.originalSet(); + } } diff --git a/core/src/main/java/jef/operations/LimitOp.java b/core/src/main/java/jef/operations/LimitOp.java index ac9565e..6bb5929 100644 --- a/core/src/main/java/jef/operations/LimitOp.java +++ b/core/src/main/java/jef/operations/LimitOp.java @@ -1,15 +1,16 @@ package jef.operations; +import jef.DbSet; import jef.Queryable; import jef.expressions.Expression; import jef.expressions.LimitExpression; import jef.expressions.SelectExpression; import jef.expressions.selectable.DatabaseSelectAllExpression; +import jef.serializable.SerializableObject; -import java.io.Serializable; import java.util.List; -public class LimitOp implements Queryable { +public class LimitOp implements Queryable { private final Queryable queryable; private Long start; private Long count; @@ -35,6 +36,11 @@ public class LimitOp implements Queryable { return getExpression().toString(); } + @Override + public DbSet originalSet() { + return queryable.originalSet(); + } + @Override public Queryable limit(long l) { count = l; diff --git a/core/src/main/java/jef/operations/Operation.java b/core/src/main/java/jef/operations/Operation.java index 35184ee..ad35ecf 100644 --- a/core/src/main/java/jef/operations/Operation.java +++ b/core/src/main/java/jef/operations/Operation.java @@ -1,9 +1,8 @@ package jef.operations; import jef.Queryable; +import jef.serializable.SerializableObject; -import java.io.Serializable; - -public interface Operation extends Queryable { +public interface Operation extends Queryable {//TODO is this class required } diff --git a/core/src/main/java/jef/operations/SortOp.java b/core/src/main/java/jef/operations/SortOp.java index 0bf7f54..6bc4ccf 100644 --- a/core/src/main/java/jef/operations/SortOp.java +++ b/core/src/main/java/jef/operations/SortOp.java @@ -1,5 +1,6 @@ package jef.operations; +import jef.DbSet; import jef.Queryable; import jef.asm.AsmParseException; import jef.asm.AsmParser; @@ -13,14 +14,14 @@ import jef.expressions.modifier.TableAliasInjector; import jef.expressions.modifier.TernaryRewriter; import jef.expressions.selectable.DatabaseSelectAllExpression; import jef.serializable.SerializableFunction; +import jef.serializable.SerializableObject; import lombok.AllArgsConstructor; -import java.io.Serializable; import java.util.ArrayList; import java.util.List; @AllArgsConstructor -public class SortOp implements Queryable { +public class SortOp implements Queryable { private final Queryable queryable; private final List sorts = new ArrayList<>(); @@ -45,6 +46,11 @@ public class SortOp implements Queryable { return getExpression().toString(); } + @Override + public DbSet originalSet() { + return queryable.originalSet(); + } + public SortOp thenSorted(SerializableFunction fieldSelector) { var f = parseFunction(fieldSelector); this.sorts.add(new OrderExpression.Sort(f, OrderExpression.SortDirection.ASCENDING)); diff --git a/core/src/main/java/jef/platform/base/Database.java b/core/src/main/java/jef/platform/base/Database.java index 9208fe1..9e4eae4 100644 --- a/core/src/main/java/jef/platform/base/Database.java +++ b/core/src/main/java/jef/platform/base/Database.java @@ -1,25 +1,96 @@ package jef.platform.base; import jef.MigrationException; +import jef.expressions.Expression; +import jef.model.DbContext; +import jef.model.DbFieldBuilder; +import jef.model.ModelBuilder; +import jef.serializable.SerializableObject; import lombok.AllArgsConstructor; import lombok.Getter; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; @Getter @AllArgsConstructor public abstract class Database { - // private final DatabaseConfiguration config; protected final Connection connection; protected final DatabaseOptions options; public abstract void migrate() throws MigrationException; -// public ResultSet executeRaw(String s) throws SQLException { -// try (var stmt = connection.prepareStatement(s)) { -// try (var res = stmt.executeQuery()) { -// return res; -// } -// } -// } + public abstract List queryExpression(DbContext context, Class clazz, Expression expression) throws SQLException; + + public List queryRaw(DbContext context, Class clazz, String query) throws SQLException { + var modelBuilder = ModelBuilder.from(context.getClass()); + var entity = modelBuilder.entity(clazz); + var fields = entity.fields(); + try (var stmt = connection.prepareStatement(query)) { + return queryRaw(stmt, clazz, fields); + } + } + + protected List queryRaw(PreparedStatement stmt, Class clazz, List> fields) throws SQLException { + try (var result = stmt.executeQuery()) { + var ret = new ArrayList(); + while (result.next()) { + ret.add(readRow(result, clazz, fields)); + } + return ret; + } + } + + protected T readRow(ResultSet res, Class clazz, List> fields) throws SQLException { + //TODO replace with by runtime generated class + try { + var model = (T) clazz.getConstructor().newInstance(); + for (DbFieldBuilder field : fields) { + if (!field.getField().isDatabaseField()) { + continue; + } + Field f = field.getField().getField(); + if (f == null) { + System.out.println("not class member for " + field.getField().getName()); + continue; + } + f.setAccessible(true); + if (field.getField().getType() == String.class) { + f.set(model, res.getString(f.getName())); + } else if (field.getField().getType() == byte.class || field.getField().getType() == Byte.class) { + f.set(model, res.getByte(f.getName())); + } else if (field.getField().getType() == char.class || field.getField().getType() == Character.class) { +// f.set(model, res.getString(f.getName()));//TODO handle char + throw new UnsupportedOperationException("char"); + } else if (field.getField().getType() == short.class || field.getField().getType() == Short.class) { + f.set(model, res.getShort(f.getName())); + } else if (field.getField().getType() == int.class || field.getField().getType() == Integer.class) { + f.set(model, res.getInt(f.getName())); + } else if (field.getField().getType() == long.class || field.getField().getType() == Long.class) { + f.set(model, res.getLong(f.getName())); + } else if (field.getField().getType() == float.class || field.getField().getType() == Float.class) { + f.set(model, res.getFloat(f.getName())); + } else if (field.getField().getType() == double.class || field.getField().getType() == Double.class) { + f.set(model, res.getDouble(f.getName())); + } else { + throw new UnsupportedOperationException(); + } + } + return model; + } catch (IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {//TODO better handling + throw new RuntimeException(e); + } + } + + public abstract void add(DbContext context, Class clazz, T model) throws SQLException; + + public abstract void update(DbContext context, Class clazz, T model) throws SQLException; + + public abstract void delete(DbContext context, Class clazz, T model) throws SQLException; } diff --git a/core/src/main/java/jef/platform/dummy/DummyDatabase.java b/core/src/main/java/jef/platform/dummy/DummyDatabase.java index b73ea3b..b219743 100644 --- a/core/src/main/java/jef/platform/dummy/DummyDatabase.java +++ b/core/src/main/java/jef/platform/dummy/DummyDatabase.java @@ -1,7 +1,13 @@ package jef.platform.dummy; import jef.MigrationException; +import jef.expressions.Expression; +import jef.model.DbContext; import jef.platform.base.Database; +import jef.serializable.SerializableObject; + +import java.sql.SQLException; +import java.util.List; public class DummyDatabase extends Database { public DummyDatabase() { @@ -11,4 +17,28 @@ public class DummyDatabase extends Database { @Override public void migrate() throws MigrationException { } + + @Override + public List queryExpression(DbContext context, Class clazz, Expression expression) { + return List.of(); + } + + @Override + public List queryRaw(DbContext context, Class clazz, String query) { + return List.of(); + } + + @Override + public void add(DbContext context, Class clazz, T entity) throws SQLException { + } + + @Override + public void update(DbContext context, Class clazz, T model) throws SQLException { + + } + + @Override + public void delete(DbContext context, Class clazz, T model) throws SQLException { + + } } diff --git a/core/src/main/java/jef/util/Util.java b/core/src/main/java/jef/util/Util.java index 3f5883a..888f43a 100644 --- a/core/src/main/java/jef/util/Util.java +++ b/core/src/main/java/jef/util/Util.java @@ -26,6 +26,11 @@ public abstract class Util { R apply(T t, U u) throws Throwable; } + @FunctionalInterface + public interface ThrowableBiConsumer { + void accept(T t, U u) throws Throwable; + } + public interface Equality { boolean check(T t1, T t2); } diff --git a/core/src/test/java/jef/asm/access/ClassAnalyzerTest.java b/core/src/test/java/jef/asm/access/ClassAnalyzerTest.java new file mode 100644 index 0000000..a617633 --- /dev/null +++ b/core/src/test/java/jef/asm/access/ClassAnalyzerTest.java @@ -0,0 +1,58 @@ +package jef.asm.access; + +import jef.model.ModelBuilder; +import jef.model.annotations.ForeignKey; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +class ClassAnalyzerTest { + + @Test + void analyze() { + var analyzer = new ClassAnalyzer(); + var result = analyzer.analyze(Employee.class); + System.out.println(result.toStringFriendly()); + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Company extends SerializableObject { + private int id; + private String name; + + private Employee ceo; + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Employee extends SerializableObject { + private int id; + private String name; + + private Employee boss; + @ForeignKey(getterOrField = "boss") + private int bossId; + + private Company company; + @ForeignKey(getterOrField = "company") + private int companyId; + } +} \ No newline at end of file diff --git a/core/src/test/java/jef/operations/FilterOpTest.java b/core/src/test/java/jef/operations/FilterOpTest.java index de13071..22b460f 100644 --- a/core/src/test/java/jef/operations/FilterOpTest.java +++ b/core/src/test/java/jef/operations/FilterOpTest.java @@ -13,7 +13,7 @@ public class FilterOpTest { @Test public void testSimple() { String act; - act = new DbSet<>(TestClass.class, "table1") + act = new DbSet<>(null, TestClass.class, "table1") .filter(e -> true) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a WHERE 1", act); @@ -23,7 +23,7 @@ public class FilterOpTest { public void testMultipleFilter() { String act; var s = List.of(1, 3); - act = new DbSet<>(TestClass.class, "table1") + act = new DbSet<>(null, TestClass.class, "table1") .filter(e -> s.contains(e.i)) .filter(e -> e.i == 1337) .toString(); diff --git a/core/src/test/java/jef/operations/LimitOpTest.java b/core/src/test/java/jef/operations/LimitOpTest.java index 7bddb79..987913a 100644 --- a/core/src/test/java/jef/operations/LimitOpTest.java +++ b/core/src/test/java/jef/operations/LimitOpTest.java @@ -8,25 +8,25 @@ public class LimitOpTest { @Test public void test() { String act; - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .limit(10).skip(5) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a OFFSET 5 LIMIT 10", act); - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .skip(5).limit(10) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a OFFSET 5 LIMIT 10", act); - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .skip(5) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a OFFSET 5", act); - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .limit(10) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a LIMIT 10", act); diff --git a/core/src/test/java/jef/operations/SortOpTest.java b/core/src/test/java/jef/operations/SortOpTest.java index 8cf0f69..8279101 100644 --- a/core/src/test/java/jef/operations/SortOpTest.java +++ b/core/src/test/java/jef/operations/SortOpTest.java @@ -8,27 +8,27 @@ class SortOpTest { @Test public void test() { String act; - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .sorted(e -> e.getI()).thenSorted(e -> e.getD()).thenSorted(e -> e.getF()).thenSorted(e -> e.getL()).thenSorted(e -> e.getO()) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a ORDER BY `a`.`i` ASC, `a`.`d` ASC, `a`.`f` ASC, `a`.`l` ASC, `a`.`o` ASC", act); - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .sortedDescending(e -> e.getI()).thenSortedDescending(e -> e.getD()).thenSortedDescending(e -> e.getF()) /**/.thenSortedDescending(e -> e.getL()).thenSortedDescending(e -> e.getO()) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a ORDER BY `a`.`i` DESC, `a`.`d` DESC, `a`.`f` DESC, `a`.`l` DESC, `a`.`o` DESC", act); //alternating patterns - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .sortedDescending(e -> e.getI()).thenSorted(e -> e.getD()).thenSortedDescending(e -> e.getF()).thenSorted(e -> e.getL()) /**/.thenSortedDescending(e -> e.getO()) .toString(); Assertions.assertEquals("SELECT * FROM (SELECT * FROM `table1`) a ORDER BY `a`.`i` DESC, `a`.`d` ASC, `a`.`f` DESC, `a`.`l` ASC, `a`.`o` DESC", act); - act = new DbSet<>(FilterOpTest.TestClass.class, "table1") + act = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .sorted(e -> e.getI()).thenSortedDescending(e -> e.getD()).thenSorted(e -> e.getF()).thenSortedDescending(e -> e.getL()) /**/.thenSorted(e -> e.getO()) .toString(); diff --git a/core/src/test/java/jef/visitors/DebugExpressionVisitorTest.java b/core/src/test/java/jef/visitors/DebugExpressionVisitorTest.java index b3ba492..851bc13 100644 --- a/core/src/test/java/jef/visitors/DebugExpressionVisitorTest.java +++ b/core/src/test/java/jef/visitors/DebugExpressionVisitorTest.java @@ -12,11 +12,11 @@ class DebugExpressionVisitorTest { @Test public void test() { var s = List.of(1, 3); - Queryable q = new DbSet<>(FilterOpTest.TestClass.class, "table1") + Queryable q = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .filter(e -> s.contains(e.i) && e.i == 1337 && e.i == 420); new DebugExpressionVisitor().visit(q.getExpression()); - Queryable q2 = new DbSet<>(FilterOpTest.TestClass.class, "table1") + Queryable q2 = new DbSet<>(null, FilterOpTest.TestClass.class, "table1") .filter(e -> s.contains(e.i) || e.i == 1337); new DebugExpressionVisitor().visit(q2.getExpression()); } diff --git a/mysql/src/main/java/jef/platform/mysql/MysqlDatabase.java b/mysql/src/main/java/jef/platform/mysql/MysqlDatabase.java index 1f826ee..18a5824 100644 --- a/mysql/src/main/java/jef/platform/mysql/MysqlDatabase.java +++ b/mysql/src/main/java/jef/platform/mysql/MysqlDatabase.java @@ -1,13 +1,30 @@ package jef.platform.mysql; import jef.MigrationException; +import jef.expressions.Expression; +import jef.model.DbContext; +import jef.model.DbField; +import jef.model.DbFieldBuilder; +import jef.model.ModelBuilder; import jef.platform.base.Database; import jef.platform.base.DatabaseOptions; +import jef.platform.mysql.query.MysqlQueryBuilder; +import jef.serializable.SerializableObject; +import jef.util.Util; +import java.lang.reflect.Field; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; public class MysqlDatabase extends Database { private final MysqlPlatform platform; + public MysqlDatabase(Connection connection, DatabaseOptions options, MysqlPlatform platform) { super(connection, options); this.platform = platform; @@ -17,4 +34,300 @@ public class MysqlDatabase extends Database { public void migrate() throws MigrationException { platform.getMigrationApplier(connection, options).migrate(); } + + @Override + public List queryExpression(DbContext context, Class clazz, Expression expression) throws SQLException { + MysqlQueryBuilder queryBuilder = new MysqlQueryBuilder(); + queryBuilder.visit(expression); + return queryRaw(context, clazz, queryBuilder.getQuery()); + } + + @Override + public void add(DbContext context, Class clazz, T model) throws SQLException { + var modelBuilder = ModelBuilder.from(context.getClass()); + var entity = modelBuilder.entity(clazz); + + var columns = new ArrayList(); + var valueBinders = new ArrayList>(); + + for (DbFieldBuilder field : entity.fields()) { + Field f = field.getField().getField(); + if (f == null) { + if (field.getField().isDatabaseField()) { + columns.add(field.getField().getName()); + valueBinders.add((stmt, i) -> stmt.setNull(i, Types.INTEGER));//TODO set proper type + } + continue; + } + + if (!field.getField().isDatabaseField()) { + continue; + } + + f.setAccessible(true); + + columns.add(field.getField().getName()); + try { + if (f.get(model) == null) { + valueBinders.add((stmt, i) -> stmt.setNull(i, Types.INTEGER)); + } else if (field.getField().getType() == String.class) { + valueBinders.add((stmt, i) -> stmt.setString(i, (String) f.get(model))); + } else if (field.getField().getType() == byte.class || field.getField().getType() == Byte.class) { + valueBinders.add((stmt, i) -> stmt.setByte(i, (byte) f.get(model))); + } else if (field.getField().getType() == char.class || field.getField().getType() == Character.class) { +// valueSetters.add((stmt, i) -> stmt.setString(i, (char) f.get(model)));//TODO handle char + throw new UnsupportedOperationException("char"); + } else if (field.getField().getType() == short.class || field.getField().getType() == Short.class) { + valueBinders.add((stmt, i) -> stmt.setShort(i, (short) f.get(model))); + } else if (field.getField().getType() == int.class || field.getField().getType() == Integer.class) { + valueBinders.add((stmt, i) -> stmt.setInt(i, (int) f.get(model))); + } else if (field.getField().getType() == long.class || field.getField().getType() == Long.class) { + valueBinders.add((stmt, i) -> stmt.setLong(i, (long) f.get(model))); + } else if (field.getField().getType() == float.class || field.getField().getType() == Float.class) { + valueBinders.add((stmt, i) -> stmt.setFloat(i, (float) f.get(model))); + } else if (field.getField().getType() == double.class || field.getField().getType() == Double.class) { + valueBinders.add((stmt, i) -> stmt.setDouble(i, (double) f.get(model))); + } else { + throw new UnsupportedOperationException(); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + var columnsString = columns.stream().map(e -> "`" + e + "`").collect(Collectors.joining(",")); + var placeholdersString = columns.stream().map(e -> "?").collect(Collectors.joining(",")); + var query = "INSERT INTO `" + entity.name() + "` (" + columnsString + ") VALUES (" + placeholdersString + ")"; + try (var stmt = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + var i = 1; + for (Util.ThrowableBiConsumer valueBinder : valueBinders) { + try { + valueBinder.accept(stmt, i++); + } catch (Throwable e) { + if (e instanceof SQLException) { + throw (SQLException) e; + } + throw new IllegalStateException("This may not happen", e); + } + } + var added = stmt.executeUpdate(); + if (added != 1) { + throw new SQLException("Failed to insert entity"); + } + try (var res = stmt.getGeneratedKeys()) { + var primary = entity.getEntity().getPrimaryKey(); + if (primary != null) { + if (!res.next()) { + throw new SQLException("Failed to add entity"); + } + var idField = primary.getFields().get(0);//TODO this only supports primary keys, and only with one column + idField.getField().setAccessible(true); + if (idField.getField().getType() == int.class || idField.getField().getType() == Integer.class) {//TODO support more types + idField.getField().set(model, res.getInt(1)); + } else if (idField.getField().getType() == long.class || idField.getField().getType() == Long.class) { + idField.getField().set(model, res.getLong(1)); + } else { + throw new UnsupportedOperationException(); + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void update(DbContext context, Class clazz, T model) throws SQLException { + var modelBuilder = ModelBuilder.from(context.getClass()); + var entity = modelBuilder.entity(clazz); + if (entity.getEntity().getPrimaryKey() == null) { + throw new UnsupportedOperationException("Primary key required for updates");//at least for now + } + + var columns = new ArrayList(); + var valueBinders = new ArrayList>(); + + for (DbFieldBuilder field : entity.fields()) { + Field f = field.getField().getField(); + if (f == null) { + if (field.getField().isDatabaseField()) { + columns.add(field.getField().getName()); + valueBinders.add((stmt, i) -> stmt.setNull(i, Types.INTEGER));//TODO set proper type + } + continue; + } + + if (!field.getField().isDatabaseField()) { + continue; + } + + f.setAccessible(true); + + columns.add(field.getField().getName()); + try { + if (f.get(model) == null) { + valueBinders.add((stmt, i) -> stmt.setNull(i, Types.INTEGER)); + } else if (field.getField().getType() == String.class) { + valueBinders.add((stmt, i) -> stmt.setString(i, (String) f.get(model))); + } else if (field.getField().getType() == byte.class || field.getField().getType() == Byte.class) { + valueBinders.add((stmt, i) -> stmt.setByte(i, (byte) f.get(model))); + } else if (field.getField().getType() == char.class || field.getField().getType() == Character.class) { +// valueSetters.add((stmt, i) -> stmt.setString(i, (char) f.get(model)));//TODO handle char + throw new UnsupportedOperationException("char"); + } else if (field.getField().getType() == short.class || field.getField().getType() == Short.class) { + valueBinders.add((stmt, i) -> stmt.setShort(i, (short) f.get(model))); + } else if (field.getField().getType() == int.class || field.getField().getType() == Integer.class) { + valueBinders.add((stmt, i) -> stmt.setInt(i, (int) f.get(model))); + } else if (field.getField().getType() == long.class || field.getField().getType() == Long.class) { + valueBinders.add((stmt, i) -> stmt.setLong(i, (long) f.get(model))); + } else if (field.getField().getType() == float.class || field.getField().getType() == Float.class) { + valueBinders.add((stmt, i) -> stmt.setFloat(i, (float) f.get(model))); + } else if (field.getField().getType() == double.class || field.getField().getType() == Double.class) { + valueBinders.add((stmt, i) -> stmt.setDouble(i, (double) f.get(model))); + } else { + throw new UnsupportedOperationException(); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + var updatesString = columns.stream().map(e -> "`" + e + "` = ?").collect(Collectors.joining(", ")); + var whereString = entity.getEntity().getPrimaryKey().getFields().stream().map(e -> "`" + e.getName() + "` = ?").collect(Collectors.joining(" AND ")); + + //add values for where clause + for (DbField field : entity.getEntity().getPrimaryKey().getFields()) { + Field f = field.getField(); + f.setAccessible(true); + try { + if (f.get(model) == null) { + valueBinders.add((stmt, i) -> stmt.setNull(i, Types.INTEGER)); + } else if (field.getField().getType() == String.class) { + valueBinders.add((stmt, i) -> stmt.setString(i, (String) f.get(model))); + } else if (field.getField().getType() == byte.class || field.getField().getType() == Byte.class) { + valueBinders.add((stmt, i) -> stmt.setByte(i, (byte) f.get(model))); + } else if (field.getField().getType() == char.class || field.getField().getType() == Character.class) { +// valueSetters.add((stmt, i) -> stmt.setString(i, (char) f.get(model)));//TODO handle char + throw new UnsupportedOperationException("char"); + } else if (field.getField().getType() == short.class || field.getField().getType() == Short.class) { + valueBinders.add((stmt, i) -> stmt.setShort(i, (short) f.get(model))); + } else if (field.getField().getType() == int.class || field.getField().getType() == Integer.class) { + valueBinders.add((stmt, i) -> stmt.setInt(i, (int) f.get(model))); + } else if (field.getField().getType() == long.class || field.getField().getType() == Long.class) { + valueBinders.add((stmt, i) -> stmt.setLong(i, (long) f.get(model))); + } else if (field.getField().getType() == float.class || field.getField().getType() == Float.class) { + valueBinders.add((stmt, i) -> stmt.setFloat(i, (float) f.get(model))); + } else if (field.getField().getType() == double.class || field.getField().getType() == Double.class) { + valueBinders.add((stmt, i) -> stmt.setDouble(i, (double) f.get(model))); + } else { + throw new UnsupportedOperationException(); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + var query = "UPDATE `" + entity.name() + "` SET " + updatesString + " WHERE " + whereString; + System.out.println(query); + try (var stmt = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + var i = 1; + for (Util.ThrowableBiConsumer valueBinder : valueBinders) { + try { + valueBinder.accept(stmt, i++); + } catch (Throwable e) { + if (e instanceof SQLException) { + throw (SQLException) e; + } + throw new IllegalStateException("This may not happen", e); + } + } + var updated = stmt.executeUpdate(); + if (updated != 1) { + throw new SQLException("Failed to update entity"); + } + try (var res = stmt.getGeneratedKeys()) { +// var primary = entity.getEntity().getPrimaryKey(); +// if (primary != null) { +// if (!res.next()) { +// throw new SQLException("Failed to add entity"); +// } +// var idField = primary.getFields().get(0);//TODO this only supports primary keys, and only with one column +// idField.getField().setAccessible(true); +// if (idField.getField().getType() == int.class || idField.getField().getType() == Integer.class) {//TODO support more types +// idField.getField().set(model, res.getInt(1)); +// } else if (idField.getField().getType() == long.class || idField.getField().getType() == Long.class) { +// idField.getField().set(model, res.getLong(1)); +// } else { +// throw new UnsupportedOperationException(); +// } +// } +// } catch (IllegalAccessException e) { +// throw new RuntimeException(e); + } + } + } + + @Override + public void delete(DbContext context, Class clazz, T model) throws SQLException { + var modelBuilder = ModelBuilder.from(context.getClass()); + var entity = modelBuilder.entity(clazz); + if (entity.getEntity().getPrimaryKey() == null) { + throw new UnsupportedOperationException("Primary key required for updates");//at least for now + } + + var valueBinders = new ArrayList>(); + var whereString = entity.getEntity().getPrimaryKey().getFields().stream().map(e -> "`" + e.getName() + "` = ?").collect(Collectors.joining(" AND ")); + + //add values for where clause + for (DbField field : entity.getEntity().getPrimaryKey().getFields()) { + Field f = field.getField(); + f.setAccessible(true); + try { + if (f.get(model) == null) { + valueBinders.add((stmt, i) -> stmt.setNull(i, Types.INTEGER)); + } else if (field.getField().getType() == String.class) { + valueBinders.add((stmt, i) -> stmt.setString(i, (String) f.get(model))); + } else if (field.getField().getType() == byte.class || field.getField().getType() == Byte.class) { + valueBinders.add((stmt, i) -> stmt.setByte(i, (byte) f.get(model))); + } else if (field.getField().getType() == char.class || field.getField().getType() == Character.class) { +// valueSetters.add((stmt, i) -> stmt.setString(i, (char) f.get(model)));//TODO handle char + throw new UnsupportedOperationException("char"); + } else if (field.getField().getType() == short.class || field.getField().getType() == Short.class) { + valueBinders.add((stmt, i) -> stmt.setShort(i, (short) f.get(model))); + } else if (field.getField().getType() == int.class || field.getField().getType() == Integer.class) { + valueBinders.add((stmt, i) -> stmt.setInt(i, (int) f.get(model))); + } else if (field.getField().getType() == long.class || field.getField().getType() == Long.class) { + valueBinders.add((stmt, i) -> stmt.setLong(i, (long) f.get(model))); + } else if (field.getField().getType() == float.class || field.getField().getType() == Float.class) { + valueBinders.add((stmt, i) -> stmt.setFloat(i, (float) f.get(model))); + } else if (field.getField().getType() == double.class || field.getField().getType() == Double.class) { + valueBinders.add((stmt, i) -> stmt.setDouble(i, (double) f.get(model))); + } else { + throw new UnsupportedOperationException(); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + var query = "DELETE FROM `" + entity.name() + "` WHERE " + whereString; + System.out.println(query); + try (var stmt = connection.prepareStatement(query)) { + var i = 1; + for (Util.ThrowableBiConsumer valueBinder : valueBinders) { + try { + valueBinder.accept(stmt, i++); + } catch (Throwable e) { + if (e instanceof SQLException) { + throw (SQLException) e; + } + throw new IllegalStateException("This may not happen", e); + } + } + var deleted = stmt.executeUpdate(); + if (deleted != 1) { + throw new SQLException("Failed to delete entity"); + } + } + } } diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java new file mode 100644 index 0000000..3b0ef80 --- /dev/null +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/ByteCodeHelper.java @@ -0,0 +1,423 @@ +package jef.platform.mysql.dbinterface; + +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.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public abstract class ByteCodeHelper { + public final static Type T_BOOL = Type.getType(boolean.class); + public final static Type T_BYTE = Type.getType(byte.class); + public final static Type T_CHAR = Type.getType(char.class); + public final static Type T_CLASS = Type.getType(Class.class); + public final static Type T_CONNECTION = Type.getType(Connection.class); + public final static Type T_DOUBLE = Type.getType(double.class); + public final static Type T_FIELD = Type.getType(Field.class); + public final static Type T_FLOAT = Type.getType(float.class); + public final static Type T_INT = Type.getType(int.class); + public final static Type T_LONG = Type.getType(long.class); + public final static Type T_MYSQL_ENTITY_DB_INTERFACE = Type.getType(MysqlEntityDbInterface.class); + public final static Type T_OBJECT = Type.getType(Object.class); + public final static Type T_OVERRIDE = Type.getType(Override.class); + public final static Type T_PREPARED_STATEMENT = Type.getType(PreparedStatement.class); + public final static Type T_RESULT_SET = Type.getType(ResultSet.class); + public final static Type T_SHORT = Type.getType(short.class); + public final static Type T_SQLEXCEPTION = Type.getType(SQLException.class); + public final static Type T_STRING = Type.getType(String.class); + public final static Type T_VOID = Type.getType(void.class); + + //region getter + private static void visitGetFieldDirectAccess(MethodVisitor mv, Field field, int varIndexEntity) { + mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); + mv.visitFieldInsn(Opcodes.GETFIELD, Type.getInternalName(field.getDeclaringClass()), field.getName(), Type.getInternalName(field.getType())); + } + + private static void visitGetter(MethodVisitor mv, GetterDescription getter, int varIndexEntity) { + mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + getter.getMethodDeclaringClassName().replace(".", "/"), + getter.getMethodName(), + "(" + Arrays.stream(getter.getMethodParameterClassNames()) + .map(ByteCodeHelper::getDescriptorForClassName) + .collect(Collectors.joining()) + + ")" + 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); + + // 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); + mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); + var clazz = field.getType(); + if (clazz == boolean.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getBoolean", Type.getMethodDescriptor(T_BOOL, T_OBJECT), false); + } else if (clazz == byte.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getByte", Type.getMethodDescriptor(T_BYTE, T_OBJECT), false); + } else if (clazz == char.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getCharacter", Type.getMethodDescriptor(T_CHAR, T_OBJECT), false); + } else if (clazz == short.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getShort", Type.getMethodDescriptor(T_SHORT, T_OBJECT), false); + } else if (clazz == int.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getInt", Type.getMethodDescriptor(T_INT, T_OBJECT), false); + } else if (clazz == long.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getLong", Type.getMethodDescriptor(T_LONG, T_OBJECT), false); + } else if (clazz == float.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getFloat", Type.getMethodDescriptor(T_FLOAT, T_OBJECT), false); + } else if (clazz == double.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "getDouble", Type.getMethodDescriptor(T_DOUBLE, T_OBJECT), false); + } else { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "get", Type.getMethodDescriptor(T_OBJECT, T_OBJECT), false); + } + } + + static void visitGetField(MethodVisitor mv, Field field, int varIndexEntity, int varIndexField, ClassDescription classDescription) { + //try direct member access + if (Modifier.isPublic(field.getModifiers())) {//TODO better check if publicly accessible + visitGetFieldDirectAccess(mv, field, varIndexEntity); + return; + } + + //try finding getter + Optional getter = classDescription.getDeclaredGetters().stream() + .filter(e -> e.getFieldDeclaringClassName().equals(field.getDeclaringClass().getName()) + && e.getFieldName().equals(field.getName())) + .findFirst(); + if (getter.isPresent()) { + visitGetter(mv, getter.get(), varIndexEntity); + return; + } + + //use reflection + visitGetViaReflection(mv, field, varIndexEntity, varIndexField); + } + //endregion + + //region setter + private static void visitSetFieldDirectAccess(MethodVisitor mv, Field field, Consumer value, int varIndexEntity) { + mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); + value.accept(mv); + mv.visitFieldInsn(Opcodes.PUTFIELD, Type.getInternalName(field.getDeclaringClass()), field.getName(), Type.getInternalName(field.getType())); + } + + private static void visitSetter(MethodVisitor mv, SetterDescription setter, Consumer value, int varIndexEntity) { + mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); + value.accept(mv); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + setter.getMethodDeclaringClassName().replace(".", "/"), + setter.getMethodName(), + "(" + Arrays.stream(setter.getMethodParameterClassNames()) + .map(ByteCodeHelper::getDescriptorForClassName) + .collect(Collectors.joining()) + + ")" + 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); + + // field.set*(entity, value); + mv.visitVarInsn(Opcodes.ALOAD, varIndexField); + mv.visitVarInsn(Opcodes.ALOAD, varIndexEntity); + value.accept(mv); + var clazz = field.getType(); + if (clazz == boolean.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setBoolean", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_BOOL), false); + } else if (clazz == byte.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setByte", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_BYTE), true); + } else if (clazz == char.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setCharacter", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_CHAR), true); + } else if (clazz == short.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setShort", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_SHORT), true); + } else if (clazz == int.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setInt", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_INT), true); + } else if (clazz == long.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setLong", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_LONG), true); + } else if (clazz == float.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setFloat", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_FLOAT), true); + } else if (clazz == double.class) { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "setDouble", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_DOUBLE), true); + } else { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FIELD.getInternalName(), "set", Type.getMethodDescriptor(T_VOID, T_OBJECT, T_OBJECT), true); + } + } + + static void visitSetField(MethodVisitor mv, Field field, Consumer value, int varIndexEntity, int varIndexField, 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); + return; + } + + //try finding getter + Optional setter = classDescription.getDeclaredSetters().stream() + .filter(e -> e.getFieldDeclaringClassName().equals(field.getDeclaringClass().getName()) + && e.getFieldName().equals(field.getName())) + .findFirst(); + if (setter.isPresent()) { + visitSetter(mv, setter.get(), value, varIndexEntity); + return; + } + + //use reflection + visitSetViaReflection(mv, field, value, varIndexEntity, varIndexField); + } + //endregion + + //region PrepareStatement + static void visitPreparedStatementSetNull(MethodVisitor mv, DbField field, int varIndexStmt, int index) { + //stmt.setNull(i, Types.*); + mv.visitVarInsn(Opcodes.ALOAD, varIndexStmt);//push stmt + visitIntInsn(mv, index);//push index + var clazz = field.getType(); + if (clazz == String.class) { + visitIntInsn(mv, Types.VARCHAR);//push Types.VARCHAR + } else if (clazz == boolean.class || clazz == Boolean.class) { + visitIntInsn(mv, Types.BIT);//push Types.BIT + } else if (clazz == byte.class || clazz == Byte.class) { + visitIntInsn(mv, Types.TINYINT);//push Types.TINYINT + } else if (clazz == char.class || clazz == Character.class) { + visitIntInsn(mv, Types.CHAR);//push Types.CHAR + } else if (clazz == short.class || clazz == Short.class) { + visitIntInsn(mv, Types.SMALLINT);//push Types.SMALLINT + } else if (clazz == int.class || clazz == Integer.class) { + visitIntInsn(mv, Types.INTEGER);//push Types.INTEGER + } else if (clazz == long.class || clazz == Long.class) { + visitIntInsn(mv, Types.BIGINT);//push Types.BIGINT + } else if (clazz == float.class || clazz == Float.class) { + visitIntInsn(mv, Types.FLOAT);//push Types.FLOAT + } else if (clazz == double.class || clazz == Double.class) { + visitIntInsn(mv, Types.DOUBLE);//push Types.DOUBLE + } else { + throw new UnsupportedOperationException(clazz.getName()); + } + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_PREPARED_STATEMENT.getInternalName(), "setNull", "(II)V", false); + } + + static void visitPreparedStatementSetAny(MethodVisitor mv, DbField field, int varIndexStmt, int index, Consumer value) { + var clazz = field.getType(); + //stmt.set*(i, value); + mv.visitVarInsn(Opcodes.ALOAD, varIndexStmt);//push stmt + visitIntInsn(mv, index);//push index + value.accept(mv); + if (clazz == String.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setString", "(ILjava.lang.String;)V", true); + } else if (clazz == boolean.class || clazz == Boolean.class) { + if (clazz == Boolean.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_BOOL.getInternalName(), "booleanValue", "()Z", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setBoolean", "(IZ)V", true); + } else if (clazz == byte.class || clazz == Byte.class) { + if (clazz == Byte.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_BYTE.getInternalName(), "byteValue", "()B", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setByte", "(IB)V", true); + } else if (clazz == char.class || clazz == Character.class) { +// if (clazz == Character.class) {//unbox +// mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_CHAR.getInternalName(), "charValue", "()C", true); +// } +// mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setCharacter", "(IC)V", true); + throw new UnsupportedOperationException("char"); + } else if (clazz == short.class || clazz == Short.class) { + if (clazz == Short.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_SHORT.getInternalName(), "shortValue", "()S", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setShort", "(IS)V", true); + } else if (clazz == int.class || clazz == Integer.class) { + if (clazz == Integer.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_INT.getInternalName(), "intValue", "()I", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setInt", "(II)V", true); + } else if (clazz == long.class || clazz == Long.class) { + if (clazz == Long.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_LONG.getInternalName(), "longValue", "()J", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setLong", "(IJ)V", true); + } else if (clazz == float.class || clazz == Float.class) { + if (clazz == Float.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_FLOAT.getInternalName(), "floatValue", "()F", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setFloat", "(IF)V", true); + } else if (clazz == double.class || clazz == Double.class) { + if (clazz == Double.class) {//unbox + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, T_DOUBLE.getInternalName(), "doubleValue", "()D", true); + } + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_PREPARED_STATEMENT.getInternalName(), "setDouble", "(ID)V", true); + } else { + throw new UnsupportedOperationException(clazz.getName()); + } + } + //endregion + + //region ResultSet + static void visitResultSetGetAny(MethodVisitor mv, DbField field, int varIndexRes) { + var clazz = field.getType(); + // res.get*(fieldname); + mv.visitVarInsn(Opcodes.ALOAD, varIndexRes);//push res + mv.visitLdcInsn(field.getName());//push 1 + if (clazz == String.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getString", "(Ljava.lang.String;)Ljava.lang.String;", true); + } else if (clazz == boolean.class || clazz == Boolean.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getBoolean", "(Ljava.lang.String;)Z", true); + if (clazz == Boolean.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_BOOL.getInternalName(), "", "(Z)V", true); + } + } else if (clazz == byte.class || clazz == Byte.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getByte", "(Ljava.lang.String;)B", true); + if (clazz == Byte.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_BYTE.getInternalName(), "", "(B)V", true); + } + } else if (clazz == char.class || clazz == Character.class) { +// mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getCharacter", "(Ljava.lang.String;)C", true); +// if (clazz == Character.class) {//unbox +// mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_CHAR.getInternalName(), "", "(C)Ljava.lang.Character;", true); +// } + throw new UnsupportedOperationException("char"); + } else if (clazz == short.class || clazz == Short.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getShort", "(Ljava.lang.String;)S", true); + if (clazz == Short.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_SHORT.getInternalName(), "", "(S)V", true); + } + } else if (clazz == int.class || clazz == Integer.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getInt", "(Ljava.lang.String;)I", true); + if (clazz == Integer.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_INT.getInternalName(), "", "(I)V", true); + } + } else if (clazz == long.class || clazz == Long.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getLong", "(Ljava.lang.String;)J", true); + if (clazz == Long.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_LONG.getInternalName(), "", "(J)V", true); + } + } else if (clazz == float.class || clazz == Float.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getFloat", "(Ljava.lang.String;)F", true); + if (clazz == Float.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_FLOAT.getInternalName(), "", "(F)V", true); + } + } else if (clazz == double.class || clazz == Double.class) { + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "getDouble", "(Ljava.lang.String;)D", true); + if (clazz == Double.class) {//box + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_DOUBLE.getInternalName(), "", "(D)V", true); + } + } else { + throw new UnsupportedOperationException(clazz.getName()); + } + + // if (res.wasNull()) { + // POP + // PUSH NULL + // } + if (!clazz.isPrimitive()) { + var labelWasNotNull = new Label(); + mv.visitVarInsn(Opcodes.ALOAD, varIndexRes);//push res + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, T_RESULT_SET.getInternalName(), "wasNull", "()Z", true); + mv.visitJumpInsn(Opcodes.IFEQ, labelWasNotNull); + mv.visitInsn(Opcodes.POP);//POP + mv.visitInsn(Opcodes.ACONST_NULL);//PUSH NULL + mv.visitLabel(labelWasNotNull); + } + } + //endregion + + //region util + static void visitIntInsn(MethodVisitor mv, int value) { + switch (value) { + case -1 -> mv.visitInsn(Opcodes.ICONST_M1); + case 0 -> mv.visitInsn(Opcodes.ICONST_0); + case 1 -> mv.visitInsn(Opcodes.ICONST_1); + case 2 -> mv.visitInsn(Opcodes.ICONST_2); + case 3 -> mv.visitInsn(Opcodes.ICONST_3); + case 4 -> mv.visitInsn(Opcodes.ICONST_4); + case 5 -> mv.visitInsn(Opcodes.ICONST_5); + default -> { + if (Byte.MIN_VALUE <= value && value <= Byte.MAX_VALUE) { + mv.visitIntInsn(Opcodes.BIPUSH, value); + } else if (Short.MIN_VALUE <= value && value <= Short.MAX_VALUE) { + mv.visitIntInsn(Opcodes.SIPUSH, value); + } else { + mv.visitLdcInsn(value); + } + } + } + } + + static void visitThrowSQLException(MethodVisitor mv, String message) { + mv.visitTypeInsn(Opcodes.NEW, T_SQLEXCEPTION.getInternalName()); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(message); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, T_SQLEXCEPTION.getInternalName(), "", Type.getMethodDescriptor(T_VOID, T_STRING), false); + mv.visitInsn(Opcodes.ATHROW); + } + + static String getDescriptorForClassName(String className) { + return switch (className) { + case "void" -> "V"; + case "boolean" -> "Z"; + case "byte" -> "B"; + case "char" -> "C"; + case "short" -> "S"; + case "int" -> "I"; + case "long" -> "J"; + case "float" -> "F"; + case "double" -> "D"; + default -> "L" + className.replace(".", "/") + ";"; + }; + } + //endregion + + //region var access + // private void visitLoadVariable(MethodVisitor mv, String className, int varIndex) { +// switch (className) { +// case "boolean", "byte", "char", "short", "int" -> mv.visitVarInsn(Opcodes.ILOAD, varIndex); +// case "long" -> mv.visitVarInsn(Opcodes.LLOAD, varIndex); +// case "float" -> mv.visitVarInsn(Opcodes.FLOAD, varIndex); +// case "double" -> mv.visitVarInsn(Opcodes.DLOAD, varIndex); +// default -> mv.visitVarInsn(Opcodes.ALOAD, varIndex); +// } +// } +// +// private void visitStoreVariable(MethodVisitor mv, String className, int varIndex) { +// switch (className) { +// case "boolean", "byte", "char", "short", "int" -> mv.visitVarInsn(Opcodes.ISTORE, varIndex); +// case "long" -> mv.visitVarInsn(Opcodes.LSTORE, varIndex); +// case "float" -> mv.visitVarInsn(Opcodes.FSTORE, varIndex); +// case "double" -> mv.visitVarInsn(Opcodes.DSTORE, varIndex); +// default -> mv.visitVarInsn(Opcodes.ASTORE, varIndex); +// } +// } + //endregion +} diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java new file mode 100644 index 0000000..051efce --- /dev/null +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterface.java @@ -0,0 +1,63 @@ +package jef.platform.mysql.dbinterface; + +import jef.serializable.SerializableObject; + +import javax.xml.transform.Result; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +public abstract class MysqlEntityDbInterface { + public void add(Connection connection, T entity) throws SQLException { + try (var stmt = queryAdd(connection)) { + bindParamsAdd(stmt, entity); + + var added = stmt.executeUpdate(); + if (added != 1) { + throw new SQLException("Failed to insert entity"); + } + + readBackGeneratedValuesAdd(stmt, entity); + } + } + + 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 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); + } + +// public void update(Connection connection, T model); +// +// public void delete(Connection connection, T model); +// +// public 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 new file mode 100644 index 0000000..c1f66e3 --- /dev/null +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGenerator.java @@ -0,0 +1,380 @@ +package jef.platform.mysql.dbinterface; + +import jef.asm.access.ClassAnalyzer; +import jef.asm.access.model.ClassDescription; +import jef.model.DbContext; +import jef.model.DbEntity; +import jef.model.DbField; +import jef.model.annotations.Generated; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +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.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class MysqlEntityDbInterfaceGenerator { + private static final boolean SET_SYNTHETIC_FLAG = false; + private static final int SYNTHETIC_FLAG = SET_SYNTHETIC_FLAG ? Opcodes.ACC_SYNTHETIC : 0; + + private final Class context; + private final DbEntity entity; + + private final ClassWriter writer; + private final ClassVisitor visitor; + + //runtime + private ClassDescription classDescription; + + //results + @Getter + private String className; + @Getter + private byte[] byteCode; + + public MysqlEntityDbInterfaceGenerator(Class context, DbEntity entity) { + this.context = context; + this.entity = entity; + writer = new ClassWriter(Opcodes.ASM9); + visitor = new ClassVisitor(Opcodes.ASM9, writer) {//TODO definitely broken here + }; + } + + public void generate() { + classDescription = new ClassAnalyzer().analyze(entity.getType()); + + //follows template "$MysqlEntityDbInterface" +// className = context.getName() + "$" + entity.getType().getSimpleName() + "" + MysqlEntityDbInterface.class.getSimpleName(); + className = entity.getType().getSimpleName() + "" + MysqlEntityDbInterface.class.getSimpleName(); + + defineClass(); + defineConstructor(); +// defineAddFunction(); + defineQueryAddFunction(); + defineBindParamsAddFunction(); + defineReadBackGeneratedValuesAddPreparedStatement(); + defineReadBackGeneratedValuesAddResultSet(); +// defineUpdateFunction(); +// defineDeleteFunction(); +// defineReadFunction(); + + visitor.visitEnd(); + + byteCode = writer.toByteArray(); + } + + 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.visitNestHost(Type.getInternalName(context)); +// visitor.visitInnerClass(className.replace(".", "/"), Type.getInternalName(context), classSimpleName, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SUPER); + } + + //public () {} + private void defineConstructor() { + var mv = visitor.visitMethod(Opcodes.ACC_PUBLIC | SYNTHETIC_FLAG, "", "()V", null, new String[0]); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, ByteCodeHelper.T_MYSQL_ENTITY_DB_INTERFACE.getInternalName(), "", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(1, 1); + 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]); + } + }); + } + 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 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()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var varIndexThis = 0; + var varIndexConnection = 1; + + var columnsAndValueBinders = prepareColumnsAndValueBindersAdd(); + + var columnsString = columnsAndValueBinders.getColumns().stream().map(e -> "`" + e + "`").collect(Collectors.joining(",")); + var placeholdersString = columnsAndValueBinders.getColumns().stream().map(e -> "?").collect(Collectors.joining(",")); + var query = "INSERT INTO `" + entity.getName() + "` (" + columnsString + ") VALUES (" + placeholdersString + ")"; + + // 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.getPrimaryKey() != null) { + mv.visitInsn(Opcodes.ICONST_1); //Statement.RETURN_GENERATED_KEYS == 1 + } 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 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()}); + 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(); + + // 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(); + var labelStartField = new Label(); + var labelEndField = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + 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++); + } + + //return + mv.visitInsn(Opcodes.RETURN); + + // variable scopes + 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.visitEnd(); + } + + private void defineReadBackGeneratedValuesAddPreparedStatement() { + if (entity.getFields().stream().anyMatch(e -> e.getGenerated() != Generated.Type.NONE)) { + 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()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var label = new Label(); + + mv.visitCode(); + + // 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.visitMaxs(0, 3);//TODO stack size + mv.visitEnd(); + } + + private void defineReadBackGeneratedValuesAddResultSet() { + if (entity.getFields().stream().noneMatch(e -> e.getGenerated() != Generated.Type.NONE)) { + 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()}); + mv.visitAnnotation(ByteCodeHelper.T_OVERRIDE.getDescriptor(), true).visitEnd(); + + var varIndexThis = 0; + var varIndexRes = 1; + var varIndexEntity = 2; + var varIndexField = 3; + + // variable scopes + var labelStartThis = new Label(); + var labelEndThis = new Label(); + var labelStartRes = new Label(); + var labelEndRes = new Label(); + var labelStartEntity = new Label(); + var labelEndEntity = new Label(); + var labelStartField = new Label(); + var labelEndField = new Label(); + + // begin + mv.visitCode(); + + // variable scopes + 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; + } + + // 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); + } + + //return + mv.visitInsn(Opcodes.RETURN); + + // variable scopes + 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.visitEnd(); + } + + private void defineUpdateFunction() { + } + + private void defineDeleteFunction() { + } + + private void defineReadFunction() { + } + +} diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceClassLoader.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceClassLoader.java new file mode 100644 index 0000000..0077240 --- /dev/null +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceClassLoader.java @@ -0,0 +1,66 @@ +package jef.platform.mysql.dbinterface; + +import jef.serializable.SerializableObject; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class MysqlInterfaceClassLoader extends ClassLoader { + private final Map resources = new HashMap<>(); + + public MysqlInterfaceClassLoader(String name, ClassLoader parent) { + super(name, parent); + } + + public MysqlInterfaceClassLoader(ClassLoader parent) { + super(parent); + } + + public MysqlInterfaceClassLoader() { + } + + public Class> defineClass(String className, byte[] byteCode) { + synchronized (resources) { + var resourceName = className.replace(".", "/") + ".class"; + resources.put(resourceName, byteCode); + } + return (Class>) super.defineClass(className, byteCode, 0, byteCode.length); + } + + @Override + public URL getResource(String name) { + return super.getResource(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + return super.getResources(name); + } + + @Override + public InputStream getResourceAsStream(String name) { + synchronized (resources) { + return super.getResourceAsStream(name); + } + } + + @Override + public Stream resources(String name) { + return super.resources(name); + } + + @Override + protected URL findResource(String moduleName, String name) throws IOException { + return super.findResource(moduleName, name); + } + + @Override + protected URL findResource(String name) { + return super.findResource(name); + } +} diff --git a/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceFactory.java b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceFactory.java new file mode 100644 index 0000000..fefa7ae --- /dev/null +++ b/mysql/src/main/java/jef/platform/mysql/dbinterface/MysqlInterfaceFactory.java @@ -0,0 +1,33 @@ +package jef.platform.mysql.dbinterface; + +import jef.model.DbContext; +import jef.model.DbEntity; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@AllArgsConstructor +public class MysqlInterfaceFactory { + private static MysqlInterfaceFactory getInstance(Class context) { + return new MysqlInterfaceFactory(context); + } + + private final Class context; + private final MysqlInterfaceClassLoader classLoader = new MysqlInterfaceClassLoader(); + private final Map, Class>> cache = new HashMap<>(); + + public Class> getInterface(DbEntity entity) { + synchronized (cache) { + return (Class>) cache.computeIfAbsent(entity.getType(), ignored -> generateInterface(entity)); + } + } + + private Class> generateInterface(DbEntity entity) { + var generator = new MysqlEntityDbInterfaceGenerator(context, entity); + generator.generate(); + var byteCode = generator.getByteCode(); + return classLoader.defineClass(generator.getClassName(), byteCode); + } +} diff --git a/mysql/src/test/java/jef/platform/mysql/MysqlSimpleIntegrativeTest.java b/mysql/src/test/java/jef/platform/mysql/MysqlSimpleIntegrativeTest.java new file mode 100644 index 0000000..0968071 --- /dev/null +++ b/mysql/src/test/java/jef/platform/mysql/MysqlSimpleIntegrativeTest.java @@ -0,0 +1,172 @@ +package jef.platform.mysql; + +import jef.DbSet; +import jef.model.DbContext; +import jef.model.DbContextOptions; +import jef.model.annotations.Clazz; +import jef.model.annotations.ForeignKey; +import jef.platform.base.Database; +import jef.platform.base.DatabaseOptions; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +import java.sql.DriverManager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MysqlSimpleIntegrativeTest { + @Test + void queryEntities() throws Exception { + //setup + Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(); + var dboptions = new DatabaseOptions("jdbc:mysql://localhost/test", "test", "password", getClass().getSimpleName(), null); + var ctxoptions = new DbContextOptions(dboptions); + var conn = DriverManager.getConnection(dboptions.getUrl(), dboptions.getUser(), dboptions.getPassword()); + var sqlPlatform = new MysqlPlatform(); + var db = new MysqlDatabase(conn, dboptions, sqlPlatform); + var ctx = new Ctx(db, ctxoptions); + + //test + var result = ctx.getCompanies().toList(); + System.out.println(result); + + //assert + + } + + @Test + void addEntity() throws Exception { + //setup + Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(); + var dboptions = new DatabaseOptions("jdbc:mysql://localhost/test", "test", "password", getClass().getSimpleName(), null); + var ctxoptions = new DbContextOptions(dboptions); + var conn = DriverManager.getConnection(dboptions.getUrl(), dboptions.getUser(), dboptions.getPassword()); + var sqlPlatform = new MysqlPlatform(); + var db = new MysqlDatabase(conn, dboptions, sqlPlatform); + var ctx = new Ctx(db, ctxoptions); + + //test + var entity = new Company(); + entity.setName("some company"); + ctx.getCompanies().add(entity); + + //assert + assertNotEquals(0, entity.getId()); + } + + @Test + void updateEntity() throws Exception { + //setup + Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(); + var dboptions = new DatabaseOptions("jdbc:mysql://localhost/test", "test", "password", getClass().getSimpleName(), null); + var ctxoptions = new DbContextOptions(dboptions); + var conn = DriverManager.getConnection(dboptions.getUrl(), dboptions.getUser(), dboptions.getPassword()); + var sqlPlatform = new MysqlPlatform(); + var db = new MysqlDatabase(conn, dboptions, sqlPlatform); + var ctx = new Ctx(db, ctxoptions); + + var entity = new Company(); + entity.setName("some company"); + ctx.getCompanies().add(entity); + assertNotEquals(0, entity.getId()); + + //test + entity.setName("other company"); + ctx.getCompanies().update(entity); + + //assert + var id = entity.getId(); +// var fromdb = ctx.getCompanies().filter(e -> e.id == entity.getId()).toList().stream().findFirst().orElseThrow(); +// var fromdb = ctx.getCompanies().filter(e -> e.id == entity.id).toList().stream().findFirst().orElseThrow(); +// var fromdb = ctx.getCompanies().filter(e -> e.id == id).toList().stream().findFirst().orElseThrow(); + var fromdb = ctx.getCompanies().toList().stream().filter(e -> e.id == id).findFirst().orElseThrow(); +// var fromdb = ctx.getCompanies().toList().stream().filter(e -> e.id == 5).findFirst().orElseThrow(); + assertEquals(entity, fromdb); + assertNotSame(entity, fromdb); + } + + @Test + void deleteEntity() throws Exception { + //setup + Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(); + var dboptions = new DatabaseOptions("jdbc:mysql://localhost/test", "test", "password", getClass().getSimpleName(), null); + var ctxoptions = new DbContextOptions(dboptions); + var conn = DriverManager.getConnection(dboptions.getUrl(), dboptions.getUser(), dboptions.getPassword()); + var sqlPlatform = new MysqlPlatform(); + var db = new MysqlDatabase(conn, dboptions, sqlPlatform); + var ctx = new Ctx(db, ctxoptions); + + var entity = new Company(); + entity.setName("some company"); + ctx.getCompanies().add(entity); + assertNotEquals(0, entity.getId()); + + //test + ctx.getCompanies().delete(entity); + + //assert + var id = entity.getId(); +// var fromdb = ctx.getCompanies().filter(e -> e.id == entity.getId()).toList().stream().findFirst().orElseThrow();//TODO get this to work too +// var fromdb = ctx.getCompanies().filter(e -> e.id == entity.id).toList().stream().findFirst().orElseThrow(); +// var fromdb = ctx.getCompanies().filter(e -> e.id == id).toList().stream().findFirst().orElseThrow(); + var fromdb = ctx.getCompanies().toList().stream().noneMatch(e -> e.id == id); + assertTrue(fromdb); + } + + @Getter + public static class Ctx extends DbContext { + @Clazz(Company.class) + private DbSet companies; + @Clazz(Employee.class) + private DbSet employees; + + public Ctx() { + } + + public Ctx(Database database, DbContextOptions options) { + super(database, options); + } + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Company extends SerializableObject { + private int id; + private String name; + + private Employee ceo; + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Employee extends SerializableObject { + private int id; + private String name; + + private Employee boss; + @ForeignKey(getterOrField = "boss") + private int bossId; + + private Company company; + @ForeignKey(getterOrField = "company") + private int companyId; + } +} \ No newline at end of file diff --git a/mysql/src/test/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGeneratorTest.java b/mysql/src/test/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGeneratorTest.java new file mode 100644 index 0000000..372d5ff --- /dev/null +++ b/mysql/src/test/java/jef/platform/mysql/dbinterface/MysqlEntityDbInterfaceGeneratorTest.java @@ -0,0 +1,86 @@ +package jef.platform.mysql.dbinterface; + +import jef.DbSet; +import jef.model.DbContext; +import jef.model.DbContextOptions; +import jef.model.ModelBuilder; +import jef.model.annotations.Clazz; +import jef.model.annotations.ForeignKey; +import jef.platform.base.Database; +import jef.serializable.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +class MysqlEntityDbInterfaceGeneratorTest { + private static final String GENERATED_MIGRATIONS_DBINTERFACES = "target/test-generated-dbinterfaces/"; +// private static final String GENERATED_MIGRATIONS_FOLDER_SRC = GENERATED_MIGRATIONS_DBINTERFACES + "src/"; + private static final String GENERATED_MIGRATIONS_FOLDER_TARGET = GENERATED_MIGRATIONS_DBINTERFACES + "target/"; + + @Test + void generate() throws IOException { + var mb = ModelBuilder.from(Ctx.class); + var generator = new MysqlEntityDbInterfaceGenerator<>(Ctx.class, mb.getEntity(Employee.class)); + generator.generate(); +// var path = Path.of(GENERATED_MIGRATIONS_FOLDER_TARGET, "MysqlEntityDbInterfaceGeneratorTest", "MysqlEntityDbInterfaceGeneratorTest$Ctx$EmployeeMysqlEntityDbInterface.class"); + var path = Path.of(GENERATED_MIGRATIONS_FOLDER_TARGET, "MysqlEntityDbInterfaceGeneratorTest", "EmployeeMysqlEntityDbInterface.class"); + path.toFile().getParentFile().mkdirs(); + Files.write(path, generator.getByteCode(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + } + + @Getter + public static class Ctx extends DbContext { + @Clazz(Company.class) + private DbSet companies; + @Clazz(Employee.class) + private DbSet employees; + + public Ctx() { + } + + public Ctx(Database database, DbContextOptions options) { + super(database, options); + } + } + +// @Getter +// @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Company extends SerializableObject { + private int id; + private String name; + + private Employee ceo; + } + +// @Getter +// @Setter + @AllArgsConstructor + @NoArgsConstructor + @ToString + @EqualsAndHashCode(callSuper = false) + public static class Employee extends SerializableObject { + private int id; + private String name; + + private Employee boss; + @ForeignKey(getterOrField = "boss") + private int bossId; + + private Company company; + @ForeignKey(getterOrField = "company") + private int companyId; + } +} \ No newline at end of file