diff --git a/README.md b/README.md index 48cba35..f4ff6e0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # Apples * universal compare tool * compares any to any and shows the diff +* no reflection diff --git a/pom.xml b/pom.xml index 16256e2..d556ec2 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,11 @@ 5.9.3 test + + org.ow2.asm + asm-tree + 9.4 + diff --git a/src/main/java/nl/sander/apples/AppleFactory.java b/src/main/java/nl/sander/apples/AppleFactory.java new file mode 100644 index 0000000..ef656a4 --- /dev/null +++ b/src/main/java/nl/sander/apples/AppleFactory.java @@ -0,0 +1,134 @@ +package nl.sander.apples; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.tree.*; + +import java.lang.reflect.Modifier; +import java.util.LinkedList; +import java.util.UUID; + +import static org.objectweb.asm.Opcodes.*; + + class AppleFactory extends ClassVisitor { + + public static final String SUPER = javaName(BaseApple.class.getName()); + + public static final String INIT = ""; + public static final String ZERO_ARGS_VOID = "()V"; + + private boolean isRecord = false; + + public AppleFactory() { + super(ASM9); + } + + final ClassNode classNode = new ClassNode(); + + private String classToMap; + + private MethodNode compareMethod; + + private int localVarIndex = 0; + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (superName.equals("java/lang/Record")) { + isRecord = true; + } + this.classToMap = name; + classNode.name = "Apple" + UUID.randomUUID(); + classNode.superName = SUPER; + classNode.version = V20; + classNode.access = ACC_PUBLIC; + MethodNode constructor = new MethodNode(ACC_PUBLIC, INIT, ZERO_ARGS_VOID, null, null); + constructor.instructions.add(new VarInsnNode(ALOAD, 0)); + constructor.instructions.add(new MethodInsnNode(INVOKESPECIAL, SUPER, INIT, ZERO_ARGS_VOID)); + constructor.instructions.add(new InsnNode(RETURN)); + classNode.methods.add(constructor); + + compareMethod = new MethodNode(ACC_PUBLIC, + "compare", "(Ljava/lang/Object;Ljava/lang/Object;)Lnl/sander/apples/Result;", null, null); + classNode.methods.add(compareMethod); + add(new VarInsnNode(ALOAD,0)); + } + + public MethodVisitor visitMethod(int access, String methodname, + String desc, String signature, String[] exceptions) { + if (!hasArgs(desc) && access == Modifier.PUBLIC && isRecord || + (methodname.startsWith("get") || (methodname.startsWith("is")) && desc.equals("()Z"))) { + int startIndex; + if (isRecord) { + startIndex = 0; + } else { + if (methodname.startsWith("is")) { + startIndex = 2; + } else { + startIndex = 3; + } + } + + visitGetter(correctName(methodname, startIndex), getReturnType(desc)); + } + return null; + } + + private void visitGetter(String getterMethodName, String returnType) { + add(new VarInsnNode(ALOAD, 1)); + add(new TypeInsnNode(CHECKCAST, javaName(classToMap))); + add(new MethodInsnNode(INVOKEVIRTUAL, classToMap, getterMethodName, "()" + returnType)); + + add(new VarInsnNode(ALOAD, 2)); + add(new TypeInsnNode(CHECKCAST, javaName(classToMap))); + add(new MethodInsnNode(INVOKEVIRTUAL, classToMap, getterMethodName, "()" + returnType)); + + add(new MethodInsnNode(INVOKESTATIC, "nl/sander/apples/Apples", "compare", "(Ljava/lang/Object;Ljava/lang/Object;)Lnl/sander/apples/Result;")); + add(new VarInsnNode(ASTORE, 3 + (localVarIndex++))); + } + + private String correctName(String getterMethodName, int startIndex) { + String tmp = getterMethodName.substring(startIndex); + return tmp.substring(0, 1).toLowerCase() + tmp.substring(1); + } + + @Override + public void visitEnd() { + if (localVarIndex < 6) { + add(new InsnNode(3 + localVarIndex)); + } else { + add(new LdcInsnNode(localVarIndex)); + } + add(new TypeInsnNode(ANEWARRAY, "nl/sander/apples/Result")); + + for (int i = 0; i < localVarIndex; i++) { + add(new InsnNode(DUP)); + if (i < 6) { + add(new InsnNode(3 + i)); + } else { + add(new LdcInsnNode(i)); + } + add(new VarInsnNode(ALOAD, 3 + i)); + add(new InsnNode(AASTORE)); + } + + add(new MethodInsnNode(INVOKESTATIC, "nl/sander/apples/Result", "merge", "([Lnl/sander/apples/Result;)Lnl/sander/apples/Result;")); + add(new InsnNode(ARETURN)); + } + + private void add(AbstractInsnNode ins) { + compareMethod.instructions.add(ins); + } + + private String getReturnType(String desc) { + return desc.substring(2); + } + + private boolean hasArgs(String desc) { + return desc.charAt(1) != ')'; + } + + private static String javaName(String className) { + return className.replaceAll("\\.", "/"); + } + +} diff --git a/src/main/java/nl/sander/apples/Apples.java b/src/main/java/nl/sander/apples/Apples.java index 5ae75ab..279c847 100644 --- a/src/main/java/nl/sander/apples/Apples.java +++ b/src/main/java/nl/sander/apples/Apples.java @@ -1,10 +1,16 @@ package nl.sander.apples; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +import java.io.FileOutputStream; import java.util.Map; public class Apples { private final static Map CHAR_ESCAPES = Map.of('\t', "\\t", '\b', "\\b", '\n', "\\n", '\r', "\\r", '\f', "\\f", '\\', "\\\\"); + private static final ByteClassLoader generatedClassesLoader = new ByteClassLoader(); + public static Result compare(Object left, Object right) { if (left == null) { return Result.from(right == null, "null != " + asString(right)); @@ -22,7 +28,27 @@ public class Apples { return Result.unequal(asString(left) + " != " + asString(right)); } - return Result.SAME; + if (left instanceof String) { + return Result.from(left.equals(right), () -> asString(left) + " != " + asString(right)); + } + try { + ClassReader cr = new ClassReader(left.getClass().getName()); + AppleFactory appleFactory = new AppleFactory(); + cr.accept(appleFactory, ClassReader.SKIP_FRAMES); + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES ); + + appleFactory.classNode.accept(classWriter); + byte[] byteArray = classWriter.toByteArray(); + + try (FileOutputStream f = new FileOutputStream("B.class")) { + f.write(byteArray); + } + generatedClassesLoader.addClass(appleFactory.classNode.name, byteArray); + BaseApple apple = (BaseApple) generatedClassesLoader.loadClass(appleFactory.classNode.name).getConstructor().newInstance(); + return apple.compare(left, right); + } catch (Exception e) { + throw new RuntimeException(e); + } } public static Result compare(long left, long right) { diff --git a/src/main/java/nl/sander/apples/BaseApple.java b/src/main/java/nl/sander/apples/BaseApple.java new file mode 100644 index 0000000..f9ac0ed --- /dev/null +++ b/src/main/java/nl/sander/apples/BaseApple.java @@ -0,0 +1,6 @@ +package nl.sander.apples; + +abstract class BaseApple { + + public abstract Result compare(T left, T right); +} diff --git a/src/main/java/nl/sander/apples/ByteClassLoader.java b/src/main/java/nl/sander/apples/ByteClassLoader.java new file mode 100644 index 0000000..872baf3 --- /dev/null +++ b/src/main/java/nl/sander/apples/ByteClassLoader.java @@ -0,0 +1,23 @@ +package nl.sander.apples; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +class ByteClassLoader extends ClassLoader { + + private final ConcurrentMap> classes = new ConcurrentHashMap<>(); + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + Class instance = classes.get(name); + if (instance == null) { + throw new ClassNotFoundException(name); + } + return instance; + } + + public void addClass(String name, byte[] bytecode) { + Class classDef = defineClass(name, bytecode, 0, bytecode.length); + classes.put(name, classDef); + } +} diff --git a/src/main/java/nl/sander/apples/Result.java b/src/main/java/nl/sander/apples/Result.java index d977f4b..4d9346b 100644 --- a/src/main/java/nl/sander/apples/Result.java +++ b/src/main/java/nl/sander/apples/Result.java @@ -1,6 +1,9 @@ package nl.sander.apples; +import java.util.Arrays; import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; public record Result(boolean areEqual, List diffs) { public static Result SAME = new Result(true, List.of()); @@ -13,8 +16,24 @@ public record Result(boolean areEqual, List diffs) { } } + public static Result from(boolean areEqual, Supplier messageSupplier) { + if (!areEqual) { + return new Result(areEqual, List.of(messageSupplier.get())); + } else { + return SAME; + } + } + public static Result unequal(String message) { return from(false, message); } + public static Result merge(Result... result) { + boolean areEqual = Arrays.stream(result).allMatch(r -> r.areEqual); + List diffs = Arrays.stream(result) + .map(Result::diffs) + .flatMap(List::stream) + .collect(Collectors.toList()); + return new Result(areEqual, diffs); + } } diff --git a/src/test/java/nl/sander/apples/Plum.java b/src/test/java/nl/sander/apples/Plum.java new file mode 100644 index 0000000..5d633e0 --- /dev/null +++ b/src/test/java/nl/sander/apples/Plum.java @@ -0,0 +1,4 @@ +package nl.sander.apples; + +public record Plum(String core, String peel, boolean juicy, int number, float price, Storage storage) { +} diff --git a/src/test/java/nl/sander/apples/PlumApple.java b/src/test/java/nl/sander/apples/PlumApple.java new file mode 100644 index 0000000..9521e6a --- /dev/null +++ b/src/test/java/nl/sander/apples/PlumApple.java @@ -0,0 +1,14 @@ +package nl.sander.apples; + +public class PlumApple extends BaseApple { + public Result compare(Plum left, Plum right) { + Result core = Apples.compare(left.core(), right.core()); + Result peel = Apples.compare(left.peel(), right.peel()); + Result juicy = Apples.compare(left.juicy(), right.juicy()); + Result price = Apples.compare(left.price(), right.price()); + Result number = Apples.compare(left.number(), right.number()); + Result storage = Apples.compare(left.storage(), right.storage()); + + return Result.merge(core, peel, juicy, price, number, storage); + } +} diff --git a/src/test/java/nl/sander/apples/RecordsTest.java b/src/test/java/nl/sander/apples/RecordsTest.java new file mode 100644 index 0000000..e9dcb41 --- /dev/null +++ b/src/test/java/nl/sander/apples/RecordsTest.java @@ -0,0 +1,33 @@ +package nl.sander.apples; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class RecordsTest { + + @Test + void testExample() { + Result comparison = new PlumApple().compare(new Plum("small", "red",true, 1, 1.0F,Storage.HIGH), + new Plum("large", "green",true, 1, 1.0F,Storage.HIGH)); + + assertFalse(comparison.areEqual()); + assertFalse(comparison.diffs().isEmpty()); + assertEquals(2, comparison.diffs().size()); + assertEquals("\"small\" != \"large\"", comparison.diffs().get(0)); + assertEquals("\"red\" != \"green\"", comparison.diffs().get(1)); + } + + @Test + void testRecords() { + Result comparison = Apples.compare(new Plum("small", "red",true, 1, 1.0F,Storage.HIGH), + new Plum("large", "green",true, 1, 1.0F,Storage.HIGH)); + + assertFalse(comparison.areEqual()); + assertFalse(comparison.diffs().isEmpty()); + assertEquals(2, comparison.diffs().size()); + assertEquals("\"small\" != \"large\"", comparison.diffs().get(0)); + assertEquals("\"red\" != \"green\"", comparison.diffs().get(1)); + } +} diff --git a/src/test/java/nl/sander/apples/Storage.java b/src/test/java/nl/sander/apples/Storage.java new file mode 100644 index 0000000..bf7f895 --- /dev/null +++ b/src/test/java/nl/sander/apples/Storage.java @@ -0,0 +1,6 @@ +package nl.sander.apples; + +public enum Storage { + HIGH, + LOW +}