Initial commit
That's a lotta stuff for an initial commit, but well...
This commit is contained in:
4
tweed5-patchwork/build.gradle.kts
Normal file
4
tweed5-patchwork/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
implementation("org.ow2.asm:asm:${properties["asm.version"]}")
|
||||
implementation("org.ow2.asm:asm-commons:${properties["asm.version"]}")
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
public interface Patchwork<S extends Patchwork<S>> {
|
||||
boolean isPatchworkPartDefined(Class<?> patchworkInterface);
|
||||
boolean isPatchworkPartSet(Class<?> patchworkInterface);
|
||||
|
||||
S copy();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.impl.ByteArrayClassLoader;
|
||||
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
|
||||
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
|
||||
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
|
||||
import lombok.*;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Value
|
||||
public class PatchworkClassCreator<P extends Patchwork<P>> {
|
||||
@NonNull
|
||||
Class<P> patchworkInterface;
|
||||
@NonNull
|
||||
PatchworkClassGenerator.Config generatorConfig;
|
||||
|
||||
public static <P extends Patchwork<P>> Builder<P> builder() {
|
||||
return new Builder<>();
|
||||
}
|
||||
|
||||
public PatchworkClass<P> createClass(Collection<Class<?>> partInterfaces) throws PatchworkClassGenerator.GenerationException {
|
||||
List<PatchworkClassPart> parts = partInterfaces.stream().map(PatchworkClassPart::new).collect(Collectors.toList());
|
||||
|
||||
PatchworkClassGenerator generator = new PatchworkClassGenerator(generatorConfig, parts);
|
||||
try {
|
||||
generator.verify();
|
||||
} catch (PatchworkClassGenerator.VerificationException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
generator.generate();
|
||||
byte[] classBytes = generator.emit();
|
||||
//noinspection unchecked
|
||||
Class<P> patchworkClass = (Class<P>) ByteArrayClassLoader.loadClass(null, classBytes);
|
||||
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
for (PatchworkClassPart part : parts) {
|
||||
try {
|
||||
MethodHandle setterHandle = lookup.findSetter(patchworkClass, part.fieldName(), part.partInterface());
|
||||
part.fieldSetter(setterHandle);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new IllegalStateException("Failed to access setter for patchwork part " + part.partInterface().getName(), e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
MethodHandle constructorHandle = lookup.findConstructor(patchworkClass, MethodType.methodType(Void.TYPE));
|
||||
|
||||
return PatchworkClass.<P>builder()
|
||||
.classPackage(generatorConfig.classPackage())
|
||||
.className(generator.className())
|
||||
.theClass(patchworkClass)
|
||||
.constructor(constructorHandle)
|
||||
.parts(parts)
|
||||
.build();
|
||||
|
||||
} catch (NoSuchMethodException | IllegalAccessException e) {
|
||||
throw new IllegalStateException("Failed to access constructor of patchwork class", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Builder<P extends Patchwork<P>> {
|
||||
@NonNull
|
||||
private Class<P> patchworkInterface;
|
||||
@NonNull
|
||||
private String classPackage;
|
||||
private String classPrefix = "";
|
||||
|
||||
public PatchworkClassCreator<P> build() {
|
||||
return new PatchworkClassCreator<>(
|
||||
patchworkInterface,
|
||||
new PatchworkClassGenerator.Config(classPackage)
|
||||
.classPrefix(classPrefix)
|
||||
.markerInterfaces(Collections.singletonList(patchworkInterface))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
public class PatchworkPartIsNullException extends RuntimeException {
|
||||
public PatchworkPartIsNullException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
|
||||
public class ByteArrayClassLoader extends ClassLoader {
|
||||
public static Class<?> loadClass(String binaryClassName, byte[] byteCode) {
|
||||
return new ByteArrayClassLoader(ByteArrayClassLoader.class.getClassLoader())
|
||||
.createClass(binaryClassName, byteCode);
|
||||
}
|
||||
|
||||
private ByteArrayClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
public Class<?> createClass(String binaryClassName, byte[] byteCode) {
|
||||
Class<?> clazz = defineClass(binaryClassName, byteCode, 0, byteCode.length);
|
||||
resolveClass(clazz);
|
||||
return clazz;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.util.Collection;
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
public class PatchworkClass<P extends Patchwork<P>> {
|
||||
String classPackage;
|
||||
String className;
|
||||
Class<P> theClass;
|
||||
MethodHandle constructor;
|
||||
Collection<PatchworkClassPart> parts;
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkPartIsNullException;
|
||||
import de.siphalor.tweed5.patchwork.impl.util.StreamUtils;
|
||||
import lombok.*;
|
||||
import org.objectweb.asm.*;
|
||||
import org.objectweb.asm.commons.GeneratorAdapter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Getter
|
||||
public class PatchworkClassGenerator {
|
||||
/**
|
||||
* Class version to use (Java 8)
|
||||
*/
|
||||
private static final int CLASS_VERSION = Opcodes.V1_8;
|
||||
private static final String TARGET_PACKAGE = "de.siphalor.tweed5.core.generated.contextextensions";
|
||||
private static final List<Type> DEFAULT_PATHWORK_INTERFACES = Collections.singletonList(Type.getType(Patchwork.class));
|
||||
|
||||
private static final String INNER_EQUALS_METHOD_NAME = "patchwork$innerEquals";
|
||||
|
||||
private static String generateUniqueIdentifier() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
return uuid.toString().replace("-", "");
|
||||
}
|
||||
|
||||
private final Config config;
|
||||
private final Collection<PatchworkClassPart> parts;
|
||||
private final String className;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final ClassWriter classWriter;
|
||||
|
||||
public PatchworkClassGenerator(Config config, Collection<PatchworkClassPart> parts) {
|
||||
this.config = config;
|
||||
this.parts = parts;
|
||||
className = config.classPrefix() + generateUniqueIdentifier();
|
||||
classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
|
||||
}
|
||||
|
||||
public String internalClassName() {
|
||||
return config.classPackage().replace('.', '/') + "/" + className();
|
||||
}
|
||||
|
||||
public String binaryClassName() {
|
||||
return config.classPackage() + "." + className();
|
||||
}
|
||||
|
||||
public void verify() throws VerificationException {
|
||||
for (PatchworkClassPart part : parts) {
|
||||
verifyClass(part.partInterface());
|
||||
}
|
||||
verifyPartMethods();
|
||||
}
|
||||
|
||||
private void verifyClass(Class<?> partClass) throws InvalidPatchworkPartClassException {
|
||||
if (!partClass.isInterface()) {
|
||||
throw new InvalidPatchworkPartClassException(partClass, "Must be an interface");
|
||||
}
|
||||
if ((partClass.getModifiers() & Modifier.PUBLIC) == 0) {
|
||||
throw new InvalidPatchworkPartClassException(partClass, "Interface must be public");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyPartMethods() throws DuplicateMethodsException {
|
||||
Map<MethodDescriptor, Collection<Method>> methodsBySignature = new HashMap<>();
|
||||
|
||||
for (PatchworkClassPart patchworkPart : parts) {
|
||||
for (Method method : patchworkPart.partInterface().getMethods()) {
|
||||
MethodDescriptor signature = new MethodDescriptor(method.getName(), method.getParameterTypes());
|
||||
|
||||
methodsBySignature
|
||||
.computeIfAbsent(signature, s -> new ArrayList<>())
|
||||
.add(method);
|
||||
}
|
||||
}
|
||||
|
||||
List<Method> duplicateMethods = methodsBySignature.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().size() > 1)
|
||||
.flatMap(entry -> entry.getValue().stream())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!duplicateMethods.isEmpty()) {
|
||||
throw new DuplicateMethodsException(duplicateMethods);
|
||||
}
|
||||
}
|
||||
|
||||
public void generate() throws GenerationException {
|
||||
beginClass();
|
||||
generateSimpleConstructor();
|
||||
for (PatchworkClassPart extensionClass : parts) {
|
||||
addPart(extensionClass);
|
||||
}
|
||||
appendPojoMethods();
|
||||
appendDefaultPatchworkMethods();
|
||||
classWriter.visitEnd();
|
||||
}
|
||||
|
||||
public byte[] emit() {
|
||||
return classWriter.toByteArray();
|
||||
}
|
||||
|
||||
private void beginClass() {
|
||||
String[] interfaces = StreamUtils.concat(
|
||||
Stream.of(Type.getInternalName(Patchwork.class)),
|
||||
config.markerInterfaces().stream().map(Type::getInternalName),
|
||||
parts.stream().map(ext -> Type.getInternalName(ext.partInterface()))
|
||||
).toArray(String[]::new);
|
||||
|
||||
classWriter.visit(
|
||||
CLASS_VERSION,
|
||||
Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_FINAL,
|
||||
internalClassName(),
|
||||
null,
|
||||
Type.getInternalName(Object.class),
|
||||
interfaces
|
||||
);
|
||||
}
|
||||
|
||||
private void generateSimpleConstructor() {
|
||||
MethodVisitor methodWriter = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
methodWriter.visitCode();
|
||||
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
methodWriter.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
methodWriter.visitInsn(Opcodes.RETURN);
|
||||
methodWriter.visitMaxs(1, 1);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
private void appendPojoMethods() {
|
||||
appendEqualsMethod();
|
||||
appendHashCodeMethod();
|
||||
appendToStringMethod();
|
||||
}
|
||||
|
||||
// <editor-fold desc="POJO Methods">
|
||||
private void appendEqualsMethod() {
|
||||
appendInnerEqualsMethod();
|
||||
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
"equals",
|
||||
"(Ljava/lang/Object;)Z",
|
||||
null,
|
||||
null
|
||||
);
|
||||
methodWriter.visitParameter("other", Opcodes.ACC_FINAL);
|
||||
methodWriter.visitCode();
|
||||
|
||||
|
||||
Label falseLabel = methodWriter.newLabel();
|
||||
Label continueLabel = methodWriter.newLabel();
|
||||
|
||||
methodWriter.loadArg(0);
|
||||
methodWriter.loadThis();
|
||||
methodWriter.visitJumpInsn(Opcodes.IF_ACMPNE, continueLabel);
|
||||
methodWriter.push(true);
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitLabel(continueLabel);
|
||||
methodWriter.loadArg(0);
|
||||
methodWriter.visitTypeInsn(Opcodes.INSTANCEOF, internalClassName());
|
||||
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
|
||||
|
||||
methodWriter.loadArg(0);
|
||||
methodWriter.visitTypeInsn(Opcodes.CHECKCAST, internalClassName());
|
||||
methodWriter.loadThis();
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKESPECIAL,
|
||||
internalClassName(),
|
||||
INNER_EQUALS_METHOD_NAME,
|
||||
"(L" + internalClassName() + ";)Z",
|
||||
false
|
||||
);
|
||||
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
|
||||
|
||||
methodWriter.push(true);
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitLabel(falseLabel);
|
||||
methodWriter.push(false);
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitMaxs(0, 0);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
private void appendInnerEqualsMethod() {
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PRIVATE,
|
||||
INNER_EQUALS_METHOD_NAME,
|
||||
"(L" + internalClassName() + ";)Z",
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
methodWriter.visitParameter("other", Opcodes.ACC_FINAL);
|
||||
methodWriter.visitCode();
|
||||
|
||||
Label falseLabel = methodWriter.newLabel();
|
||||
for (PatchworkClassPart part : parts) {
|
||||
methodWriter.loadArg(0);
|
||||
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
|
||||
methodWriter.loadThis();
|
||||
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
Type.getInternalName(Objects.class),
|
||||
"equals",
|
||||
"(Ljava/lang/Object;Ljava/lang/Object;)Z",
|
||||
false
|
||||
);
|
||||
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
|
||||
}
|
||||
|
||||
methodWriter.push(true);
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitLabel(falseLabel);
|
||||
methodWriter.push(false);
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitMaxs(0, 0);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
private void appendHashCodeMethod() {
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
"hashCode",
|
||||
"()I",
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
methodWriter.visitCode();
|
||||
|
||||
methodWriter.push(parts.size());
|
||||
methodWriter.newArray(Type.getType(Object.class));
|
||||
|
||||
int i = 0;
|
||||
for (PatchworkClassPart part : parts) {
|
||||
methodWriter.dup();
|
||||
methodWriter.push(i);
|
||||
|
||||
methodWriter.loadThis();
|
||||
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
|
||||
|
||||
methodWriter.visitInsn(Opcodes.AASTORE);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
"java/util/Objects",
|
||||
"hash",
|
||||
"([Ljava/lang/Object;)I",
|
||||
false
|
||||
);
|
||||
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitMaxs(0, 0);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
private void appendToStringMethod() {
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
"toString",
|
||||
"()Ljava/lang/String;",
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
methodWriter.visitCode();
|
||||
|
||||
String stringBuilderType = Type.getInternalName(StringBuilder.class);
|
||||
methodWriter.visitTypeInsn(Opcodes.NEW, stringBuilderType);
|
||||
methodWriter.dup();
|
||||
methodWriter.push(className().length() + 10 + parts.size() * 64);
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKESPECIAL,
|
||||
stringBuilderType,
|
||||
"<init>",
|
||||
"(I)V",
|
||||
false
|
||||
);
|
||||
|
||||
StringBuilder constantConcat = new StringBuilder();
|
||||
constantConcat.append(className()).append("{\n");
|
||||
|
||||
for (PatchworkClassPart part : parts) {
|
||||
constantConcat.append("\t").append(part.partInterface().getSimpleName()).append(": ");
|
||||
methodWriter.push(constantConcat.toString());
|
||||
constantConcat.setLength(0);
|
||||
visitStringBuilderAppendString(methodWriter);
|
||||
|
||||
Label nullLabel = methodWriter.newLabel();
|
||||
Label continueLabel = methodWriter.newLabel();
|
||||
methodWriter.loadThis();
|
||||
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
|
||||
methodWriter.dup();
|
||||
methodWriter.visitJumpInsn(Opcodes.IFNULL, nullLabel);
|
||||
|
||||
visitToString(methodWriter);
|
||||
methodWriter.visitJumpInsn(Opcodes.GOTO, continueLabel);
|
||||
|
||||
methodWriter.visitLabel(nullLabel);
|
||||
methodWriter.pop();
|
||||
methodWriter.push("<unset>");
|
||||
|
||||
methodWriter.visitLabel(continueLabel);
|
||||
visitStringBuilderAppendString(methodWriter);
|
||||
|
||||
constantConcat.append(",\n");
|
||||
}
|
||||
constantConcat.append("}");
|
||||
methodWriter.push(constantConcat.toString());
|
||||
visitStringBuilderAppendString(methodWriter);
|
||||
visitToString(methodWriter);
|
||||
methodWriter.returnValue();
|
||||
|
||||
methodWriter.visitMaxs(0, 0);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
private void appendDefaultPatchworkMethods() {
|
||||
appendCopyMethod();
|
||||
appendIsPatchworkPartDefinedMethod();
|
||||
appendIsPatchworkPartSetMethod();
|
||||
}
|
||||
|
||||
// <editor-fold desc="Patchwork Methods">
|
||||
private void appendCopyMethod() {
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
"copy",
|
||||
"()L" + Type.getInternalName(Patchwork.class) + ";",
|
||||
null,
|
||||
null
|
||||
);
|
||||
methodWriter.visitCode();
|
||||
methodWriter.visitTypeInsn(Opcodes.NEW, internalClassName());
|
||||
methodWriter.dup();
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKESPECIAL,
|
||||
internalClassName(),
|
||||
"<init>",
|
||||
"()V",
|
||||
false
|
||||
);
|
||||
for (PatchworkClassPart part : parts) {
|
||||
methodWriter.dup();
|
||||
methodWriter.loadThis();
|
||||
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
|
||||
visitFieldInsn(methodWriter, part, Opcodes.PUTFIELD);
|
||||
}
|
||||
methodWriter.returnValue();
|
||||
methodWriter.visitMaxs(0, 0);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
private void appendIsPatchworkPartDefinedMethod() {
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
"isPatchworkPartDefined",
|
||||
"(Ljava/lang/Class;)Z",
|
||||
null,
|
||||
null
|
||||
);
|
||||
methodWriter.visitParameter(null, Opcodes.ACC_FINAL);
|
||||
methodWriter.visitCode();
|
||||
Label trueLabel = methodWriter.newLabel();
|
||||
for (PatchworkClassPart part : parts) {
|
||||
methodWriter.loadArg(0);
|
||||
methodWriter.push(Type.getType(part.partInterface()));
|
||||
methodWriter.ifCmp(Type.getType(Object.class), GeneratorAdapter.EQ, trueLabel);
|
||||
}
|
||||
methodWriter.push(false);
|
||||
methodWriter.returnValue();
|
||||
methodWriter.visitLabel(trueLabel);
|
||||
methodWriter.push(true);
|
||||
methodWriter.returnValue();
|
||||
methodWriter.visitMaxs(0, 0);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
private void appendIsPatchworkPartSetMethod() {
|
||||
GeneratorAdapter methodWriter = createMethod(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
"isPatchworkPartSet",
|
||||
"(Ljava/lang/Class;)Z",
|
||||
null,
|
||||
null
|
||||
);
|
||||
methodWriter.visitParameter(null, Opcodes.ACC_FINAL);
|
||||
methodWriter.visitCode();
|
||||
Label[] labels = new Label[parts.size()];
|
||||
int i = 0;
|
||||
for (PatchworkClassPart part : parts) {
|
||||
labels[i] = methodWriter.newLabel();
|
||||
methodWriter.loadArg(0);
|
||||
methodWriter.push(Type.getType(part.partInterface()));
|
||||
methodWriter.ifCmp(Type.getType(Object.class), GeneratorAdapter.EQ, labels[i]);
|
||||
i++;
|
||||
}
|
||||
methodWriter.push(false);
|
||||
methodWriter.returnValue();
|
||||
|
||||
Label falseLabel = methodWriter.newLabel();
|
||||
i = 0;
|
||||
for (PatchworkClassPart part : parts) {
|
||||
methodWriter.visitLabel(labels[i]);
|
||||
methodWriter.loadThis();
|
||||
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
|
||||
methodWriter.push((String) null);
|
||||
methodWriter.ifCmp(Type.getType(part.partInterface()), GeneratorAdapter.EQ, falseLabel);
|
||||
methodWriter.push(true);
|
||||
methodWriter.returnValue();
|
||||
i++;
|
||||
}
|
||||
methodWriter.visitLabel(falseLabel);
|
||||
methodWriter.push(false);
|
||||
methodWriter.returnValue();
|
||||
methodWriter.visitMaxs(1, 1);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
|
||||
public void addPart(PatchworkClassPart patchworkPart) throws GenerationException {
|
||||
patchworkPart.fieldName("f_" + generateUniqueIdentifier());
|
||||
|
||||
classWriter.visitField(
|
||||
Opcodes.ACC_PUBLIC,
|
||||
patchworkPart.fieldName(),
|
||||
patchworkPart.partInterface().descriptorString(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
appendPartMethods(patchworkPart);
|
||||
}
|
||||
|
||||
private void appendPartMethods(PatchworkClassPart patchworkPart) throws GenerationException {
|
||||
try {
|
||||
ClassReader classReader = new ClassReader(patchworkPart.partInterface().getName());
|
||||
classReader.accept(new PartClassVisitor(patchworkPart), ClassReader.SKIP_FRAMES);
|
||||
} catch (IOException e) {
|
||||
throw new GenerationException("Failed to read interface class file", e);
|
||||
}
|
||||
}
|
||||
// </editor-fold>
|
||||
|
||||
private GeneratorAdapter createMethod(int access, String name, String desc, String signature, String[] exceptions) {
|
||||
MethodVisitor methodVisitor = classWriter.visitMethod(access, name, desc, signature, exceptions);
|
||||
return new GeneratorAdapter(methodVisitor, access, name, desc);
|
||||
}
|
||||
|
||||
private void visitFieldInsn(MethodVisitor methodWriter, PatchworkClassPart part, int opcode) {
|
||||
methodWriter.visitFieldInsn(
|
||||
opcode,
|
||||
internalClassName(),
|
||||
part.fieldName(),
|
||||
Type.getDescriptor(part.partInterface())
|
||||
);
|
||||
}
|
||||
|
||||
private static void visitToString(MethodVisitor methodWriter) {
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKEVIRTUAL,
|
||||
Type.getInternalName(Object.class),
|
||||
"toString",
|
||||
"()Ljava/lang/String;",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static void visitStringBuilderAppendString(MethodVisitor methodWriter) {
|
||||
String stringBuilderType = Type.getInternalName(StringBuilder.class);
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKEVIRTUAL,
|
||||
stringBuilderType,
|
||||
"append",
|
||||
"(Ljava/lang/String;)L" + stringBuilderType + ";",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private class PartClassVisitor extends ClassVisitor {
|
||||
private final PatchworkClassPart extensionClass;
|
||||
|
||||
protected PartClassVisitor(PatchworkClassPart extensionClass) {
|
||||
super(Opcodes.ASM9);
|
||||
this.extensionClass = extensionClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
GeneratorAdapter methodWriter = createMethod(Opcodes.ACC_PUBLIC, name, descriptor, signature, exceptions);
|
||||
return new PartMethodVisitor(api, methodWriter, descriptor, extensionClass);
|
||||
}
|
||||
}
|
||||
|
||||
private class PartMethodVisitor extends MethodVisitor {
|
||||
private final GeneratorAdapter methodWriter;
|
||||
private final String methodDescriptor;
|
||||
private final PatchworkClassPart patchworkPart;
|
||||
|
||||
protected PartMethodVisitor(int api, GeneratorAdapter methodWriter, String methodDescriptor, PatchworkClassPart patchworkPart) {
|
||||
super(api);
|
||||
this.methodWriter = methodWriter;
|
||||
this.patchworkPart = patchworkPart;
|
||||
this.methodDescriptor = methodDescriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitParameter(String name, int access) {
|
||||
methodWriter.visitParameter(name, access);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
Label nullLabel = methodWriter.newLabel();
|
||||
|
||||
methodWriter.visitCode();
|
||||
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
methodWriter.visitFieldInsn(Opcodes.GETFIELD, internalClassName(), patchworkPart.fieldName(), Type.getDescriptor(patchworkPart.partInterface()));
|
||||
methodWriter.dup();
|
||||
methodWriter.ifNull(nullLabel);
|
||||
methodWriter.loadArgs();
|
||||
methodWriter.visitMethodInsn(
|
||||
Opcodes.INVOKEINTERFACE,
|
||||
Type.getInternalName(patchworkPart.partInterface()),
|
||||
methodWriter.getName(),
|
||||
methodDescriptor,
|
||||
true
|
||||
);
|
||||
methodWriter.returnValue();
|
||||
methodWriter.visitLabel(nullLabel);
|
||||
methodWriter.pop();
|
||||
methodWriter.throwException(
|
||||
Type.getType(PatchworkPartIsNullException.class),
|
||||
"The patchwork part " + patchworkPart.partInterface().getSimpleName() + " has not been set"
|
||||
);
|
||||
|
||||
methodWriter.visitMaxs(-1, -1);
|
||||
methodWriter.visitEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
private static class MethodDescriptor {
|
||||
String name;
|
||||
Class<?>[] parameterTypes;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Config {
|
||||
@NonNull
|
||||
private String classPackage;
|
||||
@NonNull
|
||||
private String classPrefix = "";
|
||||
@NonNull
|
||||
private Collection<Class<?>> markerInterfaces = Collections.emptyList();
|
||||
}
|
||||
|
||||
public static class VerificationException extends Exception {
|
||||
private VerificationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class InvalidPatchworkPartClassException extends VerificationException {
|
||||
Class<?> partClass;
|
||||
|
||||
public InvalidPatchworkPartClassException(Class<?> partClass, String message) {
|
||||
super("Invalid patchwork part class " + partClass.getName() + ": " + message);
|
||||
this.partClass = partClass;
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class DuplicateMethodsException extends VerificationException {
|
||||
transient Collection<Method> signatures;
|
||||
|
||||
private DuplicateMethodsException(Collection<Method> methods) {
|
||||
super("Duplicate method signatures:\n" + methods.stream().map(DuplicateMethodsException::getMethodMessage).collect(Collectors.joining("\n")));
|
||||
this.signatures = methods;
|
||||
}
|
||||
|
||||
private static String getMethodMessage(Method method) {
|
||||
StringBuilder stringBuilder = new StringBuilder("\t- " + method.getDeclaringClass().getCanonicalName() + "#(");
|
||||
for (Class<?> parameterType : method.getParameterTypes()) {
|
||||
stringBuilder.append(parameterType.getCanonicalName());
|
||||
stringBuilder.append(", ");
|
||||
}
|
||||
stringBuilder.append(")");
|
||||
stringBuilder.append(method.getReturnType().getCanonicalName());
|
||||
stringBuilder.append("\n");
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class GenerationException extends Exception {
|
||||
public GenerationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
|
||||
@Data
|
||||
public class PatchworkClassPart {
|
||||
private final Class<?> partInterface;
|
||||
private String fieldName;
|
||||
private MethodHandle fieldSetter;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.siphalor.tweed5.patchwork.impl.util;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class StreamUtils {
|
||||
@SafeVarargs
|
||||
public static <T> Stream<T> concat(Stream<T>... streams) {
|
||||
Stream<T> result = Stream.empty();
|
||||
for (Stream<T> stream : streams) {
|
||||
result = Stream.concat(result, stream);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkPartIsNullException;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PatchworkClassGeneratorGeneratedClassTest {
|
||||
|
||||
PatchworkClassPart partA;
|
||||
PatchworkClassPart partB;
|
||||
|
||||
byte[] bytes;
|
||||
Class<Patchwork<?>> patchworkClass;
|
||||
Patchwork<?> patchwork;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
partA = new PatchworkClassPart(ExtensionA.class);
|
||||
partB = new PatchworkClassPart(ExtensionB.class);
|
||||
PatchworkClassGenerator generator = new PatchworkClassGenerator(
|
||||
new PatchworkClassGenerator.Config("de.siphalor.tweed5.core.test")
|
||||
.classPrefix("FullTest$")
|
||||
.markerInterfaces(Collections.singletonList(MarkerInterface.class)),
|
||||
Arrays.asList(partA, partB)
|
||||
);
|
||||
|
||||
assertDoesNotThrow(generator::verify);
|
||||
assertDoesNotThrow(generator::generate);
|
||||
|
||||
bytes = generator.emit();
|
||||
//noinspection unchecked
|
||||
patchworkClass = (Class<Patchwork<?>>) assertDoesNotThrow(() -> ByteArrayClassLoader.loadClass(null, bytes));
|
||||
|
||||
patchwork = createPatchworkInstance();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Dumping the class is only for testing purposes")
|
||||
void dumpClass() {
|
||||
try {
|
||||
Path target = File.createTempFile("tweed5-patchwork", ".class").toPath();
|
||||
try (OutputStream os = Files.newOutputStream(target)) {
|
||||
os.write(bytes);
|
||||
}
|
||||
System.out.println("Dumped generated class to " + target);
|
||||
|
||||
} catch (IOException e) {
|
||||
assertNull(e, "Must not throw exception");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void packageName() {
|
||||
assertEquals("de.siphalor.tweed5.core.test", patchworkClass.getPackage().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void className() {
|
||||
assertTrue(patchworkClass.getSimpleName().startsWith("FullTest$"), "Generated class name must start with prefix FullTest$, got " + patchworkClass.getSimpleName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void implementsInterfaces() {
|
||||
assertImplements(MarkerInterface.class, patchworkClass);
|
||||
assertImplements(Patchwork.class, patchworkClass);
|
||||
assertImplements(ExtensionA.class, patchworkClass);
|
||||
assertImplements(ExtensionB.class, patchworkClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringMethod() {
|
||||
int defaultHashCode = System.identityHashCode(patchwork);
|
||||
String stringResult = patchwork.toString();
|
||||
assertFalse(stringResult.contains(Integer.toHexString(defaultHashCode)), "Expected toString not to be the default toString, got: " + stringResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringContent() {
|
||||
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
|
||||
((ExtensionA) patchwork).setText("Hello World!");
|
||||
String stringResult = patchwork.toString();
|
||||
assertTrue(stringResult.contains("Hello World!"), "Expected toString to contain the toString of its extensions, got: " + stringResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashCodeMethod() {
|
||||
int emptyHashCode = patchwork.hashCode();
|
||||
|
||||
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
|
||||
int hashCodeWithA = patchwork.hashCode();
|
||||
|
||||
((ExtensionA) patchwork).setText("Hello World!");
|
||||
int hashCodeWithAContent = patchwork.hashCode();
|
||||
|
||||
assertNotEquals(emptyHashCode, hashCodeWithA, "Expected hashCode to be different, got: " + emptyHashCode + " and " + hashCodeWithA);
|
||||
assertNotEquals(emptyHashCode, hashCodeWithAContent, "Expected hashCode to be different, got: " + emptyHashCode + " and " + hashCodeWithAContent);
|
||||
assertNotEquals(hashCodeWithA, hashCodeWithAContent, "Expected hashCode to be different, got: " + hashCodeWithA + " and " + hashCodeWithAContent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SimplifiableAssertion")
|
||||
@Test
|
||||
void equalsMethod() {
|
||||
//noinspection ConstantValue,SimplifiableAssertion
|
||||
assertFalse(patchwork.equals(null));
|
||||
|
||||
//noinspection EqualsWithItself
|
||||
assertTrue(patchwork.equals(patchwork));
|
||||
|
||||
Patchwork<?> other = createPatchworkInstance();
|
||||
assertTrue(patchwork.equals(other));
|
||||
assertTrue(other.equals(patchwork));
|
||||
|
||||
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
|
||||
assertFalse(patchwork.equals(other));
|
||||
|
||||
setFieldValue(other, partA.fieldName(), new ExtensionAImpl());
|
||||
assertTrue(patchwork.equals(other));
|
||||
assertTrue(other.equals(patchwork));
|
||||
|
||||
((ExtensionA) patchwork).setText("Hello 1!");
|
||||
((ExtensionA) other).setText("Hello 2!");
|
||||
assertFalse(patchwork.equals(other));
|
||||
}
|
||||
|
||||
@Test
|
||||
void copy() {
|
||||
ExtensionAImpl aImpl = new ExtensionAImpl();
|
||||
setFieldValue(patchwork, partA.fieldName(), aImpl);
|
||||
|
||||
Patchwork<?> copy = patchwork.copy();
|
||||
Object aValue = getFieldValue(copy, partA.fieldName());
|
||||
Object bValue = getFieldValue(copy, partB.fieldName());
|
||||
|
||||
assertSame(aImpl, aValue);
|
||||
assertNull(bValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isPartDefined() {
|
||||
assertPartDefined(ExtensionA.class, patchwork);
|
||||
assertPartDefined(ExtensionB.class, patchwork);
|
||||
assertPartUndefined(ExtensionC.class, patchwork);
|
||||
}
|
||||
|
||||
static void assertPartDefined(Class<?> anInterface, Patchwork<?> patchwork) {
|
||||
assertTrue(patchwork.isPatchworkPartDefined(anInterface), "Patchwork part " + anInterface.getName() + " must be defined");
|
||||
}
|
||||
|
||||
static void assertPartUndefined(Class<?> anInterface, Patchwork<?> patchwork) {
|
||||
assertFalse(patchwork.isPatchworkPartDefined(anInterface), "Patchwork part " + anInterface.getName() + " must not be defined");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkFieldsExist() {
|
||||
assertFieldExists(partA.fieldName(), ExtensionA.class);
|
||||
assertFieldExists(partB.fieldName(), ExtensionB.class);
|
||||
}
|
||||
|
||||
void assertFieldExists(String fieldName, Class<?> fieldType) {
|
||||
Field field = getField(fieldName);
|
||||
assertEquals(fieldType, field.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isPartSet() {
|
||||
assertPartUnset(ExtensionA.class, patchwork);
|
||||
assertPartUnset(ExtensionB.class, patchwork);
|
||||
assertPartUnset(ExtensionC.class, patchwork);
|
||||
|
||||
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
|
||||
|
||||
assertPartSet(ExtensionA.class, patchwork);
|
||||
assertPartUnset(ExtensionB.class, patchwork);
|
||||
assertPartUnset(ExtensionC.class, patchwork);
|
||||
|
||||
setFieldValue(patchwork, partB.fieldName(), new ExtensionBImpl());
|
||||
|
||||
assertPartSet(ExtensionA.class, patchwork);
|
||||
assertPartSet(ExtensionB.class, patchwork);
|
||||
assertPartUnset(ExtensionC.class, patchwork);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inheritedMethodCalls() {
|
||||
assertThrows(PatchworkPartIsNullException.class, () -> ((ExtensionA) patchwork).getText());
|
||||
assertThrows(PatchworkPartIsNullException.class, () -> ((ExtensionB) patchwork).multiply(1, 2));
|
||||
|
||||
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
|
||||
setFieldValue(patchwork, partB.fieldName(), new ExtensionBImpl());
|
||||
|
||||
assertEquals(6, assertDoesNotThrow(() -> ((ExtensionB) patchwork).multiply(2, 3)), "Method of extension b must be working");
|
||||
|
||||
assertDoesNotThrow(() -> ((ExtensionA) patchwork).setText("something"));
|
||||
assertEquals("something", ((ExtensionA) patchwork).getText());
|
||||
}
|
||||
|
||||
Patchwork<?> createPatchworkInstance() {
|
||||
Constructor<?> constructor = assertDoesNotThrow(() -> patchworkClass.getConstructor(), "The generated class' constructor must be accessible");
|
||||
return assertDoesNotThrow(() -> (Patchwork<?>) constructor.newInstance());
|
||||
}
|
||||
|
||||
Object getFieldValue(Patchwork<?> patchwork, String fieldName) {
|
||||
return assertDoesNotThrow(() -> getField(fieldName).get(patchwork));
|
||||
}
|
||||
|
||||
void setFieldValue(Patchwork<?> patchwork, String fieldName, Object value) {
|
||||
assertDoesNotThrow(() -> getField(fieldName).set(patchwork, value), "Field " + fieldName + " must be accessible");
|
||||
}
|
||||
|
||||
Field getField(String fieldName) {
|
||||
return assertDoesNotThrow(() -> patchworkClass.getField(fieldName), "Field " + fieldName + " must exist and be public");
|
||||
}
|
||||
|
||||
static void assertPartSet(Class<?> anInterface, Patchwork<?> patchwork) {
|
||||
assertTrue(patchwork.isPatchworkPartSet(anInterface), "Patchwork part " + anInterface.getName() + " must be set");
|
||||
}
|
||||
|
||||
static void assertPartUnset(Class<?> anInterface, Patchwork<?> patchwork) {
|
||||
assertFalse(patchwork.isPatchworkPartSet(anInterface), "Patchwork part " + anInterface.getName() + " must not be set");
|
||||
}
|
||||
|
||||
static void assertImplements(Class<?> anInterface, Class<?> theClass) {
|
||||
assertTrue(anInterface.isAssignableFrom(theClass), "Class " + theClass.getName() + " must implement " + anInterface.getName());
|
||||
}
|
||||
|
||||
public interface MarkerInterface {}
|
||||
|
||||
public interface ExtensionA {
|
||||
String getText();
|
||||
|
||||
void setText(String text);
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(fluent = false)
|
||||
static class ExtensionAImpl implements ExtensionA {
|
||||
private String text;
|
||||
}
|
||||
|
||||
public interface ExtensionB {
|
||||
int multiply(int a, int b);
|
||||
}
|
||||
|
||||
static class ExtensionBImpl implements ExtensionB {
|
||||
public int multiply(int a, int b) {
|
||||
return a * b;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ExtensionC {
|
||||
void test();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class PatchworkClassGeneratorTest {
|
||||
|
||||
@Test
|
||||
void notAnInterface() {
|
||||
PatchworkClassGenerator generator = createGenerator(Collections.singletonList(NotAnInterface.class));
|
||||
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPublicInterface() {
|
||||
PatchworkClassGenerator generator = createGenerator(Collections.singletonList(NonPublicInterface.class));
|
||||
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateFields() {
|
||||
PatchworkClassGenerator generator = createGenerator(Arrays.asList(DuplicateA.class, DuplicateB.class));
|
||||
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
|
||||
}
|
||||
|
||||
PatchworkClassGenerator createGenerator(Collection<Class<?>> partClasses) {
|
||||
return new PatchworkClassGenerator(
|
||||
new PatchworkClassGenerator.Config("de.siphalor.tweed5.patchwork.test.generated"),
|
||||
partClasses.stream().map(PatchworkClassPart::new).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
public static class NotAnInterface {
|
||||
}
|
||||
|
||||
interface NonPublicInterface {
|
||||
}
|
||||
|
||||
public interface DuplicateA {
|
||||
void test();
|
||||
}
|
||||
|
||||
public interface DuplicateB {
|
||||
void test();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user