Initial commit

That's a lotta stuff for an initial commit, but well...
This commit is contained in:
2024-05-25 19:22:26 +02:00
commit b0f35b03b9
99 changed files with 6476 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
dependencies {
implementation("org.ow2.asm:asm:${properties["asm.version"]}")
implementation("org.ow2.asm:asm-commons:${properties["asm.version"]}")
}

View File

@@ -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();
}

View File

@@ -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))
);
}
}
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.patchwork.api;
public class PatchworkPartIsNullException extends RuntimeException {
public PatchworkPartIsNullException(String message) {
super(message);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}