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
+}