[patchwork, core, extensions] Hugely simplify Patchworks
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
public class InvalidPatchworkAccessException extends RuntimeException {
|
||||
public InvalidPatchworkAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
public interface Patchwork<S extends Patchwork<S>> {
|
||||
boolean isPatchworkPartDefined(Class<?> patchworkInterface);
|
||||
boolean isPatchworkPartSet(Class<?> patchworkInterface);
|
||||
import org.jetbrains.annotations.CheckReturnValue;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
S copy();
|
||||
public interface Patchwork {
|
||||
@Contract(pure = true)
|
||||
<T extends @Nullable Object> @Nullable T get(PatchworkPartAccess<T> access) throws InvalidPatchworkAccessException;
|
||||
@Contract(mutates = "this")
|
||||
<T extends @Nullable Object> void set(PatchworkPartAccess<T> access, T value) throws InvalidPatchworkAccessException;
|
||||
|
||||
@CheckReturnValue
|
||||
@Contract(pure = true)
|
||||
Patchwork copy();
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.Value;
|
||||
|
||||
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>> {
|
||||
Class<P> patchworkInterface;
|
||||
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>> {
|
||||
private Class<P> patchworkInterface;
|
||||
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,16 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.impl.PatchworkFactoryImpl;
|
||||
|
||||
public interface PatchworkFactory {
|
||||
static Builder builder() {
|
||||
return new PatchworkFactoryImpl.Builder();
|
||||
}
|
||||
|
||||
Patchwork create();
|
||||
|
||||
interface Builder {
|
||||
<T> PatchworkPartAccess<T> registerPart(Class<T> partClass);
|
||||
PatchworkFactory build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public interface PatchworkPartAccess<T extends @Nullable Object> {
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.siphalor.tweed5.patchwork.api;
|
||||
|
||||
public class PatchworkPartIsNullException extends RuntimeException {
|
||||
public PatchworkPartIsNullException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class ByteArrayClassLoader extends ClassLoader {
|
||||
public static Class<?> loadClass(@Nullable String binaryClassName, byte[] byteCode) {
|
||||
return new ByteArrayClassLoader(ByteArrayClassLoader.class.getClassLoader())
|
||||
.createClass(binaryClassName, byteCode);
|
||||
}
|
||||
|
||||
private ByteArrayClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
public Class<?> createClass(@Nullable String binaryClassName, byte[] byteCode) {
|
||||
Class<?> clazz = defineClass(binaryClassName, byteCode, 0, byteCode.length);
|
||||
resolveClass(clazz);
|
||||
return clazz;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,653 +0,0 @@
|
||||
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.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
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 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,
|
||||
@Nullable String signature,
|
||||
String @Nullable [] 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 {
|
||||
@lombok.NonNull
|
||||
private @NonNull String classPackage;
|
||||
private String classPrefix = "";
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class GenerationException extends Exception {
|
||||
public GenerationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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,100 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.InvalidPatchworkAccessException;
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class PatchworkFactoryImpl implements PatchworkFactory {
|
||||
private final UUID factoryUuid;
|
||||
private final Class<?>[] partClasses;
|
||||
|
||||
@Override
|
||||
public Patchwork create() {
|
||||
return new PatchworkImpl(new Object[partClasses.length]);
|
||||
}
|
||||
|
||||
public static class Builder implements PatchworkFactory.Builder {
|
||||
private final UUID factoryUuid = UUID.randomUUID();
|
||||
private final List<Class<?>> partClasses = new ArrayList<>();
|
||||
private boolean built;
|
||||
|
||||
@Override
|
||||
public <T> PatchworkPartAccess<T> registerPart(Class<T> partClass) {
|
||||
requireFresh();
|
||||
partClasses.add(partClass);
|
||||
return new PartAccessImpl<>(factoryUuid, partClasses.size() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchworkFactory build() {
|
||||
requireFresh();
|
||||
built = true;
|
||||
return new PatchworkFactoryImpl(factoryUuid, partClasses.toArray(new Class<?>[0]));
|
||||
}
|
||||
|
||||
private void requireFresh() {
|
||||
if (built) {
|
||||
throw new IllegalStateException("Builder has already been used.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class PartAccessImpl<T extends @Nullable Object> implements PatchworkPartAccess<T> {
|
||||
private final UUID factoryUuid;
|
||||
private final int partIndex;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
private class PatchworkImpl implements Patchwork {
|
||||
private final @Nullable Object[] partValues;
|
||||
|
||||
@Override
|
||||
public <T extends @Nullable Object> T get(PatchworkPartAccess<T> access) throws InvalidPatchworkAccessException {
|
||||
PartAccessImpl<T> castedAccess = validatePartAccess(access);
|
||||
//noinspection unchecked
|
||||
return (T) partValues[castedAccess.partIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends @Nullable Object> void set(PatchworkPartAccess<T> access, T value) throws InvalidPatchworkAccessException {
|
||||
PartAccessImpl<T> castedAccess = validatePartAccess(access);
|
||||
|
||||
if (value != null && !partClasses[castedAccess.partIndex].isInstance(value)) {
|
||||
throw new IllegalArgumentException(
|
||||
"value " + value + " of type " + value.getClass().getName() +
|
||||
" doesn't match registered value class " + partClasses[castedAccess.partIndex].getName()
|
||||
);
|
||||
}
|
||||
|
||||
partValues[castedAccess.partIndex] = value;
|
||||
}
|
||||
|
||||
private <T extends @Nullable Object> PartAccessImpl<T> validatePartAccess(PatchworkPartAccess<T> access)
|
||||
throws InvalidPatchworkAccessException {
|
||||
if (!(access instanceof PartAccessImpl<?>)) {
|
||||
throw new InvalidPatchworkAccessException("Part access is of incorrect class.");
|
||||
} else if (((PartAccessImpl<?>) access).factoryUuid != factoryUuid) {
|
||||
throw new InvalidPatchworkAccessException("Part access does not belong to the same patchwork factory.");
|
||||
}
|
||||
return (PartAccessImpl<T>) access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Patchwork copy() {
|
||||
return new PatchworkImpl(Arrays.copyOf(partValues, partValues.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package de.siphalor.tweed5.patchwork.impl;
|
||||
|
||||
import de.siphalor.tweed5.patchwork.api.InvalidPatchworkAccessException;
|
||||
import de.siphalor.tweed5.patchwork.api.Patchwork;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class PatchworkFactoryImplTest {
|
||||
@Test
|
||||
void test() {
|
||||
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
|
||||
PatchworkPartAccess<String> stringAccess1 = builder.registerPart(String.class);
|
||||
PatchworkPartAccess<@Nullable Number> integerAccess1 = builder.registerPart(Number.class);
|
||||
PatchworkPartAccess<String> stringAccess2 = builder.registerPart(String.class);
|
||||
PatchworkFactory factory = builder.build();
|
||||
|
||||
Patchwork patchwork = factory.create();
|
||||
|
||||
assertThat(patchwork.get(stringAccess1)).isNull();
|
||||
assertThat(patchwork.get(integerAccess1)).isNull();
|
||||
assertThat(patchwork.get(stringAccess2)).isNull();
|
||||
|
||||
patchwork.set(stringAccess1, "Hello");
|
||||
patchwork.set(stringAccess2, "World");
|
||||
patchwork.set(integerAccess1, 123);
|
||||
|
||||
assertThat(patchwork.get(stringAccess1)).isEqualTo("Hello");
|
||||
assertThat(patchwork.get(integerAccess1)).isEqualTo(123);
|
||||
assertThat(patchwork.get(stringAccess2)).isEqualTo("World");
|
||||
|
||||
patchwork.set(integerAccess1, null);
|
||||
assertThat(patchwork.get(integerAccess1)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void copy() {
|
||||
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
|
||||
PatchworkPartAccess<String> stringAccess = builder.registerPart(String.class);
|
||||
PatchworkFactory factory = builder.build();
|
||||
|
||||
Patchwork patchwork = factory.create();
|
||||
patchwork.set(stringAccess, "Hello");
|
||||
|
||||
Patchwork copy = patchwork.copy();
|
||||
assertThat(copy.get(stringAccess)).isEqualTo("Hello");
|
||||
assertThat(copy).isEqualTo(patchwork);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateRegister() {
|
||||
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
|
||||
builder.build();
|
||||
assertThatThrownBy(() -> builder.registerPart(String.class)).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doubleBuild() {
|
||||
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
|
||||
builder.build();
|
||||
assertThatThrownBy(builder::build).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidAccess() {
|
||||
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
|
||||
builder.registerPart(String.class);
|
||||
PatchworkFactory factory = builder.build();
|
||||
|
||||
Patchwork patchwork = factory.create();
|
||||
|
||||
PatchworkFactoryImpl.Builder otherBuilder = new PatchworkFactoryImpl.Builder();
|
||||
PatchworkPartAccess<String> otherAccess = otherBuilder.registerPart(String.class);
|
||||
otherBuilder.build();
|
||||
|
||||
assertThatThrownBy(() -> patchwork.get(otherAccess)).isInstanceOf(InvalidPatchworkAccessException.class);
|
||||
assertThatThrownBy(() -> patchwork.set(otherAccess, "Hello")).isInstanceOf(InvalidPatchworkAccessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setWrongType() {
|
||||
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
|
||||
//noinspection unchecked
|
||||
PatchworkPartAccess<Object> access = ((PatchworkPartAccess<Object>)(Object) builder.registerPart(String.class));
|
||||
PatchworkFactory factory = builder.build();
|
||||
|
||||
Patchwork patchwork = factory.create();
|
||||
assertThatThrownBy(() -> patchwork.set(access, 123)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user