it works but somehow not
This commit is contained in:
parent
974165580f
commit
b48a51fc74
11 changed files with 272 additions and 1 deletions
|
|
@ -1,3 +1,4 @@
|
|||
# Apples
|
||||
* universal compare tool
|
||||
* compares any to any and shows the diff
|
||||
* no reflection
|
||||
|
|
|
|||
5
pom.xml
5
pom.xml
|
|
@ -15,6 +15,11 @@
|
|||
<version>5.9.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm-tree</artifactId>
|
||||
<version>9.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
134
src/main/java/nl/sander/apples/AppleFactory.java
Normal file
134
src/main/java/nl/sander/apples/AppleFactory.java
Normal file
|
|
@ -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 = "<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("\\.", "/");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Character, String> 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) {
|
||||
|
|
|
|||
6
src/main/java/nl/sander/apples/BaseApple.java
Normal file
6
src/main/java/nl/sander/apples/BaseApple.java
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package nl.sander.apples;
|
||||
|
||||
abstract class BaseApple<T> {
|
||||
|
||||
public abstract Result compare(T left, T right);
|
||||
}
|
||||
23
src/main/java/nl/sander/apples/ByteClassLoader.java
Normal file
23
src/main/java/nl/sander/apples/ByteClassLoader.java
Normal file
|
|
@ -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<String, Class<?>> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> diffs) {
|
||||
public static Result SAME = new Result(true, List.of());
|
||||
|
|
@ -13,8 +16,24 @@ public record Result(boolean areEqual, List<String> diffs) {
|
|||
}
|
||||
}
|
||||
|
||||
public static Result from(boolean areEqual, Supplier<String> 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<String> diffs = Arrays.stream(result)
|
||||
.map(Result::diffs)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toList());
|
||||
return new Result(areEqual, diffs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
src/test/java/nl/sander/apples/Plum.java
Normal file
4
src/test/java/nl/sander/apples/Plum.java
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package nl.sander.apples;
|
||||
|
||||
public record Plum(String core, String peel, boolean juicy, int number, float price, Storage storage) {
|
||||
}
|
||||
14
src/test/java/nl/sander/apples/PlumApple.java
Normal file
14
src/test/java/nl/sander/apples/PlumApple.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package nl.sander.apples;
|
||||
|
||||
public class PlumApple extends BaseApple<Plum> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
33
src/test/java/nl/sander/apples/RecordsTest.java
Normal file
33
src/test/java/nl/sander/apples/RecordsTest.java
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
6
src/test/java/nl/sander/apples/Storage.java
Normal file
6
src/test/java/nl/sander/apples/Storage.java
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package nl.sander.apples;
|
||||
|
||||
public enum Storage {
|
||||
HIGH,
|
||||
LOW
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue