[weaver-pojo] Implement first prototype of POJO weaving

This commit is contained in:
2024-10-20 21:30:00 +02:00
parent 37d64502ad
commit 002f59ebd0
40 changed files with 2144 additions and 31 deletions

View File

@@ -0,0 +1,6 @@
dependencies {
api(project(":tweed5-core"))
api(project(":tweed5-naming-format"))
compileOnly(project(":tweed5-default-extensions"))
compileOnly(project(":tweed5-serde-extension"))
}

View File

@@ -0,0 +1,24 @@
package de.siphalor.tweed5.weaver.pojo.api.annotation;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks this class as a class that should be woven as a {@link de.siphalor.tweed5.core.api.entry.CompoundConfigEntry}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CompoundWeaving {
/**
* The naming format to use for this POJO.
* Use {@link de.siphalor.tweed5.namingformat.api.NamingFormatProvider} to define naming formats.
* @see de.siphalor.tweed5.namingformat.impl.DefaultNamingFormatProvider
*/
String namingFormat() default "";
Class<? extends WeavableCompoundConfigEntry> entryClass() default WeavableCompoundConfigEntry.class;
}

View File

@@ -0,0 +1,26 @@
package de.siphalor.tweed5.weaver.pojo.api.annotation;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.weaver.pojo.api.weaving.CompoundPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TrivialPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PojoWeaving {
Class<? extends ConfigContainer> container() default DefaultConfigContainer.class;
Class<? extends TweedPojoWeaver>[] weavers() default {
CompoundPojoWeaver.class,
TrivialPojoWeaver.class,
};
Class<? extends TweedExtension>[] extensions() default {};
}

View File

@@ -0,0 +1,49 @@
package de.siphalor.tweed5.weaver.pojo.api.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* {@inheritDoc}
* <br />
* A constructor taking the value {@link Class} and a {@link MethodHandle} that allows to instantiate the value Class with no arguments.
*/
public interface WeavableCompoundConfigEntry<T> extends CompoundConfigEntry<T> {
static <T, C extends WeavableCompoundConfigEntry<T>> C instantiate(
Class<C> weavableClass, Class<T> valueClass, MethodHandle constructorHandle
) throws PojoWeavingException {
try {
Constructor<C> weavableEntryConstructor = weavableClass.getConstructor(Class.class, MethodHandle.class);
return weavableEntryConstructor.newInstance(valueClass, constructorHandle);
} catch (NoSuchMethodException e) {
throw new PojoWeavingException(
"Class " + weavableClass.getName() + " must have constructor with value class and value constructor",
e
);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new PojoWeavingException(
"Failed to instantiate class for weavable compound entry " + weavableClass.getName(),
e
);
}
}
void registerSubEntry(SubEntry subEntry);
@Value
@RequiredArgsConstructor
class SubEntry {
@NotNull String name;
@NotNull ConfigEntry<?> configEntry;
@NotNull MethodHandle getter;
@NotNull MethodHandle setter;
}
}

View File

@@ -0,0 +1,199 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.collection.TypedMultimap;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.namingformat.api.NamingFormatCollector;
import de.siphalor.tweed5.namingformat.api.NamingFormats;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import de.siphalor.tweed5.weaver.pojo.impl.entry.StaticPojoCompoundConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfigImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
/**
* A weaver that weaves classes with the {@link CompoundWeaving} annotation as compound entries.
*/
public class CompoundPojoWeaver implements TweedPojoWeaver {
private static final CompoundWeavingConfig DEFAULT_WEAVING_CONFIG = CompoundWeavingConfigImpl.builder()
.compoundSourceNamingFormat(NamingFormats.camelCase())
.compoundTargetNamingFormat(NamingFormats.camelCase())
.compoundEntryClass(StaticPojoCompoundConfigEntry.class)
.build();
private final NamingFormatCollector namingFormatCollector = new NamingFormatCollector();
private RegisteredExtensionData<WeavingContext.ExtensionsData, CompoundWeavingConfig> weavingConfigAccess;
public void setup(SetupContext context) {
namingFormatCollector.setupFormats();
this.weavingConfigAccess = context.registerWeavingContextExtensionData(CompoundWeavingConfig.class);
}
@Override
public @Nullable <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
if (!valueClass.isAnnotationPresent(CompoundWeaving.class)) {
return null;
}
try {
CompoundWeavingConfig weavingConfig = getOrCreateWeavingConfig(valueClass, context);
WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy();
weavingConfigAccess.set(newExtensionsData, weavingConfig);
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueClass);
WeavableCompoundConfigEntry<T> compoundEntry = instantiateCompoundEntry(introspector, weavingConfig);
Map<String, PojoClassIntrospector.Property> properties = introspector.properties();
properties.forEach((name, property) -> {
if (shouldIncludeCompoundPropertyInWeaving(property)) {
compoundEntry.registerSubEntry(weaveCompoundSubEntry(property, newExtensionsData, context));
}
});
return compoundEntry;
} catch (Exception e) {
throw new PojoWeavingException("Exception occurred trying to weave compound for class " + valueClass.getName(), e);
}
}
private CompoundWeavingConfig getOrCreateWeavingConfig(Class<?> valueClass, WeavingContext context) {
CompoundWeavingConfig parent;
if (context.extensionsData().isPatchworkPartSet(CompoundWeavingConfig.class)) {
parent = (CompoundWeavingConfig) context.extensionsData();
} else {
parent = DEFAULT_WEAVING_CONFIG;
}
CompoundWeavingConfig local = getWeavingConfigFromClassAnnotation(valueClass);
if (local == null) {
return parent;
}
return CompoundWeavingConfigImpl.withOverrides(parent, local);
}
private WeavingContext createSubContextForProperty(
PojoClassIntrospector.Property property,
String name,
WeavingContext.ExtensionsData newExtensionsData,
WeavingContext parentContext
) {
return parentContext.subContextBuilder(name)
.additionalData(createAdditionalDataFromAnnotations(property.field().getAnnotations()))
.extensionsData(newExtensionsData)
.build();
}
private TypedMultimap<Object> createAdditionalDataFromAnnotations(Annotation[] annotations) {
if (annotations.length == 0) {
return TypedMultimap.empty();
}
TypedMultimap<Object> additionalData = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
Collections.addAll(additionalData, annotations);
return TypedMultimap.unmodifiable(additionalData);
}
@Nullable
private CompoundWeavingConfig getWeavingConfigFromClassAnnotation(Class<?> clazz) {
CompoundWeaving annotation = clazz.getAnnotation(CompoundWeaving.class);
if (annotation == null) {
return null;
}
CompoundWeavingConfigImpl.CompoundWeavingConfigImplBuilder builder = CompoundWeavingConfigImpl.builder();
builder.compoundSourceNamingFormat(NamingFormats.camelCase());
if (!annotation.namingFormat().isEmpty()) {
builder.compoundTargetNamingFormat(getNamingFormatById(annotation.namingFormat()));
}
if (annotation.entryClass() != WeavableCompoundConfigEntry.class) {
builder.compoundEntryClass(annotation.entryClass());
}
return builder.build();
}
@SuppressWarnings("unchecked")
private <C> WeavableCompoundConfigEntry<C> instantiateCompoundEntry(
PojoClassIntrospector classIntrospector,
CompoundWeavingConfig weavingConfig
) {
MethodHandle valueConstructor = classIntrospector.noArgsConstructor();
if (valueConstructor == null) {
throw new PojoWeavingException("Class " + classIntrospector.type().getName() + " must have public no args constructor");
}
//noinspection rawtypes
Class<? extends WeavableCompoundConfigEntry> annotationEntryClass = weavingConfig.compoundEntryClass();
@NotNull
Class<WeavableCompoundConfigEntry<C>> weavableEntryClass = (Class<WeavableCompoundConfigEntry<C>>) (
annotationEntryClass != null
? annotationEntryClass
: StaticPojoCompoundConfigEntry.class
);
return WeavableCompoundConfigEntry.instantiate(
weavableEntryClass,
(Class<C>) classIntrospector.type(),
valueConstructor
);
}
private boolean shouldIncludeCompoundPropertyInWeaving(PojoClassIntrospector.Property property) {
return property.getter() != null && (property.setter() != null || property.isFinal());
}
private @NotNull WeavableCompoundConfigEntry.SubEntry weaveCompoundSubEntry(
PojoClassIntrospector.Property property,
WeavingContext.ExtensionsData newExtensionsData,
WeavingContext parentContext
) {
String name = convertName(property.field().getName(), (CompoundWeavingConfig) newExtensionsData);
WeavingContext subContext = createSubContextForProperty(property, name, newExtensionsData, parentContext);
ConfigEntry<?> subEntry;
if (property.isFinal()) {
// TODO
throw new UnsupportedOperationException("Final config entries are not supported in weaving yet.");
} else {
subEntry = subContext.weaveEntry(property.field().getType(), subContext);
}
return new StaticPojoCompoundConfigEntry.SubEntry(
name,
subEntry,
property.getter(),
property.setter()
);
}
private @NotNull String convertName(String name, CompoundWeavingConfig weavingConfig) {
return NamingFormat.convert(
name,
weavingConfig.compoundSourceNamingFormat(),
weavingConfig.compoundTargetNamingFormat()
);
}
private @NotNull NamingFormat getNamingFormatById(String id) {
NamingFormat namingFormat = namingFormatCollector.namingFormats().get(id);
if (namingFormat == null) {
throw new PojoWeavingException(
"Naming format \"" + id + "\" is not recognized. Available formats are: " +
namingFormatCollector.namingFormats().keySet()
);
}
return namingFormat;
}
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import org.jetbrains.annotations.Nullable;
public class TrivialPojoWeaver implements TweedPojoWeaver {
@Override
public void setup(SetupContext context) {
// nothing to set up here
}
@Override
public @Nullable <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
return new SimpleConfigEntryImpl<>(valueClass);
}
}

View File

@@ -0,0 +1,14 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import org.jetbrains.annotations.ApiStatus;
public interface TweedPojoWeaver extends TweedPojoWeavingFunction {
@ApiStatus.OverrideOnly
void setup(SetupContext context);
interface SetupContext {
<E> RegisteredExtensionData<WeavingContext.ExtensionsData, E> registerWeavingContextExtensionData(Class<E> dataClass);
}
}

View File

@@ -0,0 +1,29 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@FunctionalInterface
public interface TweedPojoWeavingFunction {
/**
* Weaves a {@link ConfigEntry} for the given value class and context.
* The returned config entry must be sealed.
* @return The resulting, sealed config entry or {@code null}, if the weaving function is not applicable to the given parameters.
*/
@Nullable
<T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context);
@FunctionalInterface
interface NonNull extends TweedPojoWeavingFunction {
/**
* {@inheritDoc}
* <br />
* The function must ensure that the resulting entry is not null, e.g., by trowing a {@link RuntimeException}.
* @return The resulting, sealed config entry.
* @throws RuntimeException when a valid config entry could not be resolved.
*/
@Override
@NotNull <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context);
}
}

View File

@@ -0,0 +1,67 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.core.api.collection.TypedMultimap;
import lombok.*;
import lombok.experimental.Accessors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
@Value
public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
@Nullable
WeavingContext parent;
ExtensionsData extensionsData;
@Getter(AccessLevel.NONE)
TweedPojoWeavingFunction.NonNull weavingFunction;
String[] path;
TypedMultimap<Object> additionalData;
public static Builder builder() {
return new Builder(null, new String[0]);
}
public static Builder builder(String baseName) {
return new Builder(null, new String[]{ baseName });
}
public Builder subContextBuilder(String subPathName) {
String[] newPath = Arrays.copyOf(path, path.length + 1);
newPath[path.length] = subPathName;
return new Builder(this, newPath)
.extensionsData(extensionsData)
.weavingFunction(weavingFunction);
}
@Override
public @NotNull <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
return weavingFunction.weaveEntry(valueClass, context);
}
public interface ExtensionsData extends Patchwork<ExtensionsData> {}
@Accessors(fluent = true, chain = true)
@Setter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class Builder {
@Nullable
private final WeavingContext parent;
private final String[] path;
private ExtensionsData extensionsData;
private TweedPojoWeavingFunction.NonNull weavingFunction;
private TypedMultimap<Object> additionalData;
public WeavingContext build() {
return new WeavingContext(
parent,
extensionsData,
weavingFunction,
path,
additionalData
);
}
}
}

View File

@@ -0,0 +1,119 @@
package de.siphalor.tweed5.weaver.pojo.impl.entry;
import de.siphalor.tweed5.core.api.entry.BaseConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class StaticPojoCompoundConfigEntry<T> extends BaseConfigEntry<T> implements WeavableCompoundConfigEntry<T> {
private final MethodHandle noArgsConstructor;
private final Map<String, SubEntry> subEntries = new HashMap<>();
private final Map<String, ConfigEntry<?>> subConfigEntries = new HashMap<>();
public StaticPojoCompoundConfigEntry(@NotNull Class<T> valueClass, @NotNull MethodHandle noArgsConstructor) {
super(valueClass);
this.noArgsConstructor = noArgsConstructor;
}
public void registerSubEntry(SubEntry subEntry) {
requireUnsealed();
subEntries.put(subEntry.name(), subEntry);
subConfigEntries.put(subEntry.name(), subEntry.configEntry());
}
@Override
public Map<String, ConfigEntry<?>> subEntries() {
return Collections.unmodifiableMap(subConfigEntries);
}
@Override
public <V> void set(T compoundValue, String key, V value) {
SubEntry subEntry = subEntries.get(key);
if (subEntry == null) {
throw new IllegalArgumentException("Unknown config entry: " + key);
}
try {
subEntry.setter().invoke(compoundValue, value);
} catch (Throwable e) {
throw new IllegalStateException("Failed to set value for config entry \"" + key + "\"", e);
}
}
@Override
public <V> V get(T compoundValue, String key) {
SubEntry subEntry = subEntries.get(key);
if (subEntry == null) {
throw new IllegalArgumentException("Unknown config entry: " + key);
}
try {
//noinspection unchecked
return (V) subEntry.getter().invoke(compoundValue);
} catch (Throwable e) {
throw new IllegalStateException("Failed to get value for config entry \"" + key + "\"", e);
}
}
@Override
public T instantiateCompoundValue() {
try {
//noinspection unchecked
return (T) noArgsConstructor.invokeExact();
} catch (Throwable e) {
throw new IllegalStateException("Failed to instantiate compound class", e);
}
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCompoundEntry(this)) {
subConfigEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) {
entry.visitInOrder(visitor);
visitor.leaveCompoundSubEntry(key);
}
});
visitor.leaveCompoundEntry(this);
}
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterCompoundEntry(this, value)) {
subEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) {
try {
Object subValue = entry.getter().invokeExact(value);
//noinspection unchecked
visitor.visitEntry((ConfigEntry<Object>) entry.configEntry(), subValue);
} catch (Throwable e) {
throw new RuntimeException("Failed to get compound sub entry value \"" + key + "\"");
}
}
});
}
}
@Override
public @NotNull T deepCopy(@NotNull T value) {
T copy = instantiateCompoundValue();
for (SubEntry subEntry : subEntries.values()) {
try {
Object subValue = subEntry.getter().invokeExact(value);
subEntry.setter().invoke(copy, subValue);
} catch (Throwable e) {
throw new RuntimeException("Failed to copy value of sub entry \"" + subEntry.name() + "\"", e);
}
}
return copy;
}
}

View File

@@ -0,0 +1,5 @@
@ApiStatus.Internal
package de.siphalor.tweed5.weaver.pojo.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,231 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class PojoClassIntrospector {
private final Class<?> clazz;
private final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
private Map<String, Property> properties;
public static PojoClassIntrospector forClass(Class<?> clazz) {
if ((clazz.getModifiers() & Modifier.PUBLIC) == 0) {
throw new IllegalStateException("Class " + clazz.getName() + " must be public");
}
return new PojoClassIntrospector(clazz);
}
public Class<?> type() {
return clazz;
}
public @Nullable MethodHandle noArgsConstructor() {
try {
return lookup.findConstructor(clazz, MethodType.methodType(void.class));
} catch (NoSuchMethodException | IllegalAccessException | SecurityException e) {
return null;
}
}
public Map<String, Property> properties() {
if (this.properties == null) {
this.properties = new HashMap<>();
Class<?> currentClass = clazz;
while (currentClass != null) {
appendClassProperties(currentClass);
currentClass = currentClass.getSuperclass();
}
}
return Collections.unmodifiableMap(this.properties);
}
private void appendClassProperties(Class<?> targetClass) {
try {
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
if (shouldIgnoreField(field)) {
continue;
}
if (!properties.containsKey(field.getName())) {
Property property = introspectProperty(field);
properties.put(property.field.getName(), property);
} else {
// TODO: logging
}
}
} catch (Exception e) {
// TODO: logging
}
}
private boolean shouldIgnoreField(Field field) {
return (field.getModifiers() & (Modifier.STATIC | Modifier.TRANSIENT)) != 0;
}
private Property introspectProperty(Field field) {
int modifiers = field.getModifiers();
return Property.builder()
.field(field)
.isFinal((modifiers & Modifier.FINAL) != 0)
.getter(findGetter(field))
.setter(findSetter(field))
.type(field.getGenericType())
.build();
}
@Nullable
private MethodHandle findGetter(Field field) {
String fieldName = field.getName();
// fluid getters
MethodHandle method = findMethod(
clazz,
new MethodDescriptor(fieldName, MethodType.methodType(field.getType()))
);
if (method != null) {
return method;
}
// boolean getters
if (field.getType() == Boolean.class || field.getType() == Boolean.TYPE) {
method = findMethod(
clazz,
new MethodDescriptor("is" + firstToUpper(fieldName), MethodType.methodType(field.getType()))
);
if (method != null) {
return method;
}
}
// classic getters
method = findMethod(
clazz,
new MethodDescriptor("get" + firstToUpper(fieldName), MethodType.methodType(field.getType()))
);
if (method != null) {
return method;
}
// public field access
int modifiers = field.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0) {
return findFieldGetter(field);
}
return null;
}
@Nullable
private MethodHandle findSetter(Field field) {
String fieldName = field.getName();
String classicSetterName = "set" + firstToUpper(fieldName);
MethodHandle method = findFirstMethod(
clazz,
// fluid
new MethodDescriptor(fieldName, MethodType.methodType(Void.TYPE, field.getType())),
// fluid + chain
new MethodDescriptor(fieldName, MethodType.methodType(field.getDeclaringClass(), field.getType())),
// classic
new MethodDescriptor(classicSetterName, MethodType.methodType(Void.TYPE, field.getType())),
// classic + chain
new MethodDescriptor(
classicSetterName,
MethodType.methodType(field.getDeclaringClass(), field.getType())
)
);
if (method != null) {
return method;
}
// public field access
int modifiers = field.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0) {
return findFieldSetter(field);
}
return null;
}
@Nullable
private MethodHandle findFirstMethod(Class<?> targetClass, MethodDescriptor... methodDescriptors) {
for (MethodDescriptor methodDescriptor : methodDescriptors) {
MethodHandle method = findMethod(targetClass, methodDescriptor);
if (method != null) {
return method;
}
}
return null;
}
@Nullable
private MethodHandle findMethod(Class<?> targetClass, MethodDescriptor methodDescriptor) {
try {
return lookup.findVirtual(targetClass, methodDescriptor.name(), methodDescriptor.methodType());
} catch (NoSuchMethodException e) {
return null;
} catch (IllegalAccessException e) {
// TODO: logging
return null;
}
}
@Nullable
private MethodHandle findFieldGetter(Field field) {
try {
return lookup.findGetter(field.getDeclaringClass(), field.getName(), field.getType());
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
// TODO: logging
return null;
}
}
@Nullable
private MethodHandle findFieldSetter(Field field) {
try {
return lookup.findSetter(field.getDeclaringClass(), field.getName(), field.getType());
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
// TODO: logging
return null;
}
}
private static String firstToUpper(String text) {
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
}
@Value
private static class MethodDescriptor {
String name;
MethodType methodType;
}
@Value
@Builder
public static class Property {
Field field;
boolean isFinal;
Type type;
@Nullable
MethodHandle getter;
@Nullable
MethodHandle setter;
}
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving;
public class PojoWeavingException extends RuntimeException {
public PojoWeavingException(String message, Throwable cause) {
super(message, cause);
}
public PojoWeavingException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,234 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import lombok.*;
import org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
/**
* A class that sets up and handles all the bits and bobs for weaving a {@link ConfigContainer} out of a POJO.
* The POJO must be annotated with {@link PojoWeaving}.
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class TweedPojoWeaverBootstrapper<T> {
private final Class<T> pojoClass;
private final ConfigContainer<T> configContainer;
private final Collection<TweedPojoWeaver> weavers;
private PatchworkClass<WeavingContext.ExtensionsData> contextExtensionsDataClass;
public static <T> TweedPojoWeaverBootstrapper<T> create(Class<T> pojoClass) {
PojoWeaving rootWeavingConfig = expectAnnotation(pojoClass, PojoWeaving.class);
//noinspection unchecked
ConfigContainer<T> configContainer = (ConfigContainer<T>) createConfigContainer((Class<? extends ConfigContainer<?>>) rootWeavingConfig.container());
Collection<TweedExtension> extensions = loadExtensions(Arrays.asList(rootWeavingConfig.extensions()));
configContainer.registerExtensions(extensions.toArray(new TweedExtension[0]));
configContainer.finishExtensionSetup();
return new TweedPojoWeaverBootstrapper<>(pojoClass, configContainer, loadWeavers(Arrays.asList(rootWeavingConfig.weavers())));
}
private static Collection<TweedExtension> loadExtensions(Collection<Class<? extends TweedExtension>> extensionClasses) {
try {
return loadSingleServices(extensionClasses);
} catch (Exception e) {
throw new PojoWeavingException("Failed to load Tweed extensions", e);
}
}
private static Collection<TweedPojoWeaver> loadWeavers(Collection<Class<? extends TweedPojoWeaver>> weaverClasses) {
List<TweedPojoWeaver> weavers = new ArrayList<>();
for (Class<? extends TweedPojoWeaver> weaverClass : weaverClasses) {
weavers.add(checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass));
}
return weavers;
}
private static ConfigContainer<?> createConfigContainer(Class<? extends ConfigContainer<?>> containerClass) {
try {
return checkImplementsAndInstantiate(ConfigContainer.class, containerClass);
} catch (Exception e) {
throw new PojoWeavingException("Failed to instantiate config container");
}
}
private static <S> Collection<S> loadSingleServices(Collection<Class<? extends S>> serviceClasses) {
Collection<S> services = new ArrayList<>(serviceClasses.size());
for (Class<? extends S> serviceClass : serviceClasses) {
try {
services.add(loadSingleService(serviceClass));
} catch (Exception e) {
throw new PojoWeavingException("Failed to instantiate single service " + serviceClass.getName(), e);
}
}
return services;
}
private static <S> S loadSingleService(Class<S> serviceClass) {
try {
ServiceLoader<S> loader = ServiceLoader.load(serviceClass);
Iterator<S> iterator = loader.iterator();
if (!iterator.hasNext()) {
throw new PojoWeavingException("Could not find any service for class " + serviceClass.getName());
}
S service = iterator.next();
if (iterator.hasNext()) {
throw new PojoWeavingException(
"Found multiple services for class " + serviceClass.getName() + ": " +
createInstanceDebugStringFromIterator(loader.iterator())
);
}
return service;
} catch (ServiceConfigurationError e) {
throw new PojoWeavingException("Failed to load service " + serviceClass.getName(), e);
}
}
private static String createInstanceDebugStringFromIterator(Iterator<?> iterator) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[ ");
while (iterator.hasNext()) {
stringBuilder.append(createInstanceDebugDescriptor(iterator.next()));
stringBuilder.append(", ");
}
stringBuilder.append(" ]");
return stringBuilder.toString();
}
private static String createInstanceDebugDescriptor(@Nullable Object object) {
if (object == null) {
return "null";
} else {
return object.getClass().getName() + "@" + System.identityHashCode(object);
}
}
private static <A extends Annotation> A expectAnnotation(Class<?> clazz, Class<A> annotationClass) {
A annotation = clazz.getAnnotation(annotationClass);
if (annotation == null) {
throw new PojoWeavingException("Annotation " + annotationClass.getName() + " must be defined on class " + clazz);
} else {
return annotation;
}
}
private static <T> T checkImplementsAndInstantiate(Class<T> superClass, Class<? extends T> clazz) {
if (!superClass.isAssignableFrom(clazz)) {
throw new PojoWeavingException("Class " + clazz.getName() + " must extend/implement " + superClass.getName());
}
return instantiate(clazz);
}
private static <T> T instantiate(Class<T> clazz) {
try {
Constructor<T> constructor = clazz.getConstructor();
return constructor.newInstance();
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException e) {
throw new PojoWeavingException("Failed to instantiate class " + clazz.getName(), e);
}
}
public ConfigContainer<T> weave() {
setupWeavers();
WeavingContext weavingContext = createWeavingContext();
ConfigEntry<T> rootEntry = this.weaveEntry(pojoClass, weavingContext);
configContainer.attachAndSealTree(rootEntry);
return configContainer;
}
private void setupWeavers() {
Map<Class<?>, RegisteredExtensionDataImpl<?>> registeredExtensions = new HashMap<>();
TweedPojoWeaver.SetupContext setupContext = new TweedPojoWeaver.SetupContext() {
@Override
public <E> RegisteredExtensionData<WeavingContext.ExtensionsData, E> registerWeavingContextExtensionData(
Class<E> dataClass
) {
RegisteredExtensionDataImpl<E> registeredExtension = new RegisteredExtensionDataImpl<>();
registeredExtensions.put(dataClass, registeredExtension);
return registeredExtension;
}
};
for (TweedPojoWeaver weaver : weavers) {
weaver.setup(setupContext);
}
PatchworkClassCreator<WeavingContext.ExtensionsData> weavingContextCreator = PatchworkClassCreator.<WeavingContext.ExtensionsData>builder()
.classPackage(this.getClass().getPackage().getName() + ".generated")
.classPrefix("WeavingContext$")
.patchworkInterface(WeavingContext.ExtensionsData.class)
.build();
try {
this.contextExtensionsDataClass = weavingContextCreator.createClass(registeredExtensions.keySet());
for (PatchworkClassPart part : this.contextExtensionsDataClass.parts()) {
RegisteredExtensionDataImpl<?> registeredExtension = registeredExtensions.get(part.partInterface());
registeredExtension.setter(part.fieldSetter());
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new PojoWeavingException("Failed to create weaving context extensions data");
}
}
private WeavingContext createWeavingContext() {
try {
WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke();
return WeavingContext.builder()
.extensionsData(extensionsData)
.weavingFunction(this::weaveEntry)
.build();
} catch (Throwable e) {
throw new PojoWeavingException("Failed to create weaving context's extension data");
}
}
private <U> ConfigEntry<U> weaveEntry(Class<U> dataClass, WeavingContext context) {
for (TweedPojoWeaver weaver : weavers) {
ConfigEntry<U> configEntry = weaver.weaveEntry(dataClass, context);
if (configEntry != null) {
configEntry.seal(configContainer);
return configEntry;
}
}
throw new PojoWeavingException("Failed to weave " + dataClass.getName() + ": No matching weavers found");
}
@Setter
private static class RegisteredExtensionDataImpl<E> implements RegisteredExtensionData<WeavingContext.ExtensionsData, E> {
private MethodHandle setter;
@Override
public void set(WeavingContext.ExtensionsData patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
}

View File

@@ -0,0 +1,15 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import org.jetbrains.annotations.Nullable;
public interface CompoundWeavingConfig {
NamingFormat compoundSourceNamingFormat();
NamingFormat compoundTargetNamingFormat();
@SuppressWarnings("rawtypes")
@Nullable
Class<? extends WeavableCompoundConfigEntry> compoundEntryClass();
}

View File

@@ -0,0 +1,27 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.Nullable;
@Builder
@Value
public class CompoundWeavingConfigImpl implements CompoundWeavingConfig {
private static final CompoundWeavingConfigImpl EMPTY = CompoundWeavingConfigImpl.builder().build();
NamingFormat compoundSourceNamingFormat;
NamingFormat compoundTargetNamingFormat;
@SuppressWarnings("rawtypes")
@Nullable
Class<? extends WeavableCompoundConfigEntry> compoundEntryClass;
public static CompoundWeavingConfigImpl withOverrides(CompoundWeavingConfig self, CompoundWeavingConfig overrides) {
return CompoundWeavingConfigImpl.builder()
.compoundSourceNamingFormat(overrides.compoundSourceNamingFormat() != null ? overrides.compoundSourceNamingFormat() : self.compoundSourceNamingFormat())
.compoundTargetNamingFormat(overrides.compoundTargetNamingFormat() != null ? overrides.compoundTargetNamingFormat() : self.compoundTargetNamingFormat())
.compoundEntryClass(overrides.compoundEntryClass() != null ? overrides.compoundEntryClass() : self.compoundEntryClass())
.build();
}
}

View File

@@ -0,0 +1,197 @@
package de.siphalor.tweed5.weaver.pojo.api;
import de.siphalor.tweed5.core.api.collection.TypedMultimap;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class TypedMultimapTest {
@Test
void size() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
assertEquals(0, map.size());
map.add("abc");
assertEquals(1, map.size());
map.add(456);
assertEquals(2, map.size());
map.add("def");
assertEquals(3, map.size());
map.remove(456);
assertEquals(2, map.size());
}
@Test
void isEmpty() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
assertTrue(map.isEmpty());
map.add("def");
assertFalse(map.isEmpty());
map.remove("def");
assertTrue(map.isEmpty());
}
@Test
void contains() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
assertFalse(map.contains(123));
map.add(456);
assertFalse(map.contains(123));
map.add(123);
assertTrue(map.contains(123));
}
@Test
void classes() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, "abc", "def", "ghi", 789L));
assertEquals(new HashSet<>(Arrays.asList(Integer.class, String.class, Long.class)), map.classes());
}
@Test
void iterator() {
TypedMultimap<Object> map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new);
map.add("abc");
map.add(123);
map.add("def");
map.add(456);
Iterator<Object> iterator = map.iterator();
assertThrows(IllegalStateException.class, iterator::remove);
assertTrue(iterator.hasNext());
assertEquals("abc", iterator.next());
iterator.remove();
assertTrue(iterator.hasNext());
assertEquals("def", iterator.next());
iterator.remove();
assertTrue(iterator.hasNext());
assertEquals(123, iterator.next());
assertTrue(iterator.hasNext());
assertEquals(456, iterator.next());
assertFalse(iterator.hasNext());
assertThrows(NoSuchElementException.class, iterator::next);
}
@Test
void toArray() {
TypedMultimap<Object> map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new);
map.add("abc");
map.add(123);
map.add("def");
map.add(456);
Object[] array = map.toArray();
assertArrayEquals(new Object[] { "abc", "def", 123, 456 }, array);
}
@Test
void toArrayProvided() {
TypedMultimap<Number> map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new);
map.add(12);
map.add(34L);
map.add(56);
map.add(78L);
@NotNull Number[] array = map.toArray(new Number[0]);
assertArrayEquals(new Object[] { 12, 56, 34L, 78L }, array);
}
@Test
void add() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), HashSet::new);
assertTrue(map.isEmpty());
map.add(123);
assertEquals(1, map.size());
map.add("abc");
assertEquals(2, map.size());
map.add(123);
assertEquals(2, map.size());
map.add("abc");
assertEquals(2, map.size());
}
@Test
void remove() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, "abc", "def"));
assertEquals(4, map.size());
map.remove("def");
assertEquals(3, map.size());
map.remove("abc");
assertEquals(2, map.size());
}
@Test
void getAll() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, "abc", "def"));
assertEquals(Arrays.asList(123, 456), map.getAll(Integer.class));
assertEquals(Arrays.asList("abc", "def"), map.getAll(String.class));
assertEquals(Collections.emptyList(), map.getAll(Long.class));
}
@Test
void removeAll() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, 789, "abc", "def"));
map.removeAll(Arrays.asList(456, "def"));
assertEquals(3, map.size());
assertEquals(Arrays.asList(123, 789), map.getAll(Integer.class));
assertEquals(Collections.singletonList("abc"), map.getAll(String.class));
map.removeAll(Arrays.asList(123, 789));
assertArrayEquals(new Object[] { "abc" }, map.toArray());
}
@Test
void containsAll() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, 789, "abc", "def"));
assertTrue(map.containsAll(Arrays.asList(456, "def")));
assertTrue(map.containsAll(Arrays.asList(123, 789)));
assertFalse(map.containsAll(Arrays.asList(404, 789)));
}
@Test
void addAll() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, 789, "abc", "def"));
assertEquals(5, map.size());
assertEquals(Arrays.asList(123, 456, 789), map.getAll(Integer.class));
map.addAll(Arrays.asList(123L, 456L));
assertEquals(Arrays.asList(123L, 456L), map.getAll(Long.class));
}
@Test
void removeAllByClass() {
TypedMultimap<Object> map = new TypedMultimap<>(new HashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L));
map.removeAll(Integer.class);
assertEquals(3, map.size());
map.removeAll(Long.class);
assertEquals(2, map.size());
map.removeAll(String.class);
assertTrue(map.isEmpty());
}
@Test
void retainAll() {
TypedMultimap<Object> map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L));
map.retainAll(Arrays.asList("abc", 456));
assertEquals(2, map.size());
assertArrayEquals(new Object[] { 456, "abc" }, map.toArray());
map.retainAll(Collections.emptyList());
assertTrue(map.isEmpty());
}
@Test
void clear() {
TypedMultimap<Object> map = new TypedMultimap<>(new LinkedHashMap<>(), ArrayList::new);
map.addAll(Arrays.asList(123, 456, 789, "abc", "def", 123L));
map.clear();
assertTrue(map.isEmpty());
}
}

View File

@@ -0,0 +1,122 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isCompoundEntryForClassWith;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@SuppressWarnings("unused")
class CompoundPojoWeaverTest {
@Test
void weave() {
CompoundPojoWeaver compoundWeaver = new CompoundPojoWeaver();
compoundWeaver.setup(new TweedPojoWeaver.SetupContext() {
@Override
public <E> RegisteredExtensionData<WeavingContext.ExtensionsData, E> registerWeavingContextExtensionData(Class<E> dataClass) {
return (patchwork, extension) -> ((ExtensionsDataMock) patchwork).weavingConfig = (CompoundWeavingConfig) extension;
}
});
WeavingContext weavingContext = WeavingContext.builder()
.extensionsData(new ExtensionsDataMock(null))
.weavingFunction(new TweedPojoWeavingFunction.NonNull() {
@Override
public @NotNull <T> ConfigEntry<T> weaveEntry(Class<T> valueClass, WeavingContext context) {
ConfigEntry<T> entry = compoundWeaver.weaveEntry(valueClass, context);
if (entry != null) {
return entry;
} else {
//noinspection unchecked
ConfigEntry<T> configEntry = mock((Class<SimpleConfigEntry<T>>) (Class<?>) SimpleConfigEntry.class);
when(configEntry.valueClass()).thenReturn(valueClass);
return configEntry;
}
}
})
.build();
ConfigEntry<Compound> resultEntry = compoundWeaver.weaveEntry(Compound.class, weavingContext);
assertThat(resultEntry).satisfies(isCompoundEntryForClassWith(Compound.class, compoundEntry -> assertThat(compoundEntry.subEntries())
.hasEntrySatisfying("an-integer", isSimpleEntryForClass(int.class))
.hasEntrySatisfying("inner-compound", isCompoundEntryForClassWith(InnerCompound1.class, innerCompound1 -> assertThat(innerCompound1.subEntries())
.hasEntrySatisfying("a-parameter", isSimpleEntryForClass(String.class))
.hasEntrySatisfying("inner-compound", isCompoundEntryForClassWith(InnerCompound2.class, innerCompound2 -> assertThat(innerCompound2.subEntries())
.hasEntrySatisfying("inner_value", isSimpleEntryForClass(InnerValue.class))
.hasSize(1)
))
.hasSize(2)
))
.hasSize(2)
));
}
@CompoundWeaving(namingFormat = "kebab_case")
public static class Compound {
public int anInteger;
public InnerCompound1 innerCompound;
}
@CompoundWeaving
public static class InnerCompound1 {
public String aParameter;
public InnerCompound2 innerCompound;
}
@CompoundWeaving(namingFormat = "snake_case")
public static class InnerCompound2 {
public InnerValue innerValue;
}
public static class InnerValue {
public Integer value;
}
@AllArgsConstructor
private static class ExtensionsDataMock implements WeavingContext.ExtensionsData, CompoundWeavingConfig {
private CompoundWeavingConfig weavingConfig;
@Override
public boolean isPatchworkPartDefined(Class<?> patchworkInterface) {
return patchworkInterface == CompoundWeavingConfig.class;
}
@Override
public boolean isPatchworkPartSet(Class<?> patchworkInterface) {
return weavingConfig != null;
}
@Override
public WeavingContext.ExtensionsData copy() {
return new ExtensionsDataMock(weavingConfig);
}
@Override
public NamingFormat compoundSourceNamingFormat() {
return weavingConfig.compoundSourceNamingFormat();
}
@Override
public NamingFormat compoundTargetNamingFormat() {
return weavingConfig.compoundTargetNamingFormat();
}
@Override
public @Nullable Class<? extends WeavableCompoundConfigEntry> compoundEntryClass() {
return weavingConfig.compoundEntryClass();
}
}
}

View File

@@ -0,0 +1,233 @@
package de.siphalor.tweed5.weaver.pojo.impl;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@SuppressWarnings({"LombokSetterMayBeUsed", "LombokGetterMayBeUsed"})
class PojoClassIntrospectorTest {
@Test
void propertiesClassicAccessors() {
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(ClassicPojo.class);
ClassicPojo instance = new ClassicPojo(123);
Map<String, PojoClassIntrospector.Property> result = assertDoesNotThrow(introspector::properties);
assertThat(result).hasSize(4)
.hasEntrySatisfying("integer", property -> {
assertThat(property.field().getName()).isEqualTo("integer");
assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class);
assertThat(property.type()).isEqualTo(Integer.TYPE);
assertThat(property.isFinal()).isTrue();
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNull();
assertThatNoException()
.isThrownBy(() -> assertThat((int) property.getter().invokeExact(instance)).isEqualTo(123));
})
.hasEntrySatisfying("str", property -> {
assertThat(property.field().getName()).isEqualTo("str");
assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class);
assertThat(property.type()).isEqualTo(String.class);
assertThat(property.isFinal()).isFalse();
assertThat(property.getter()).isNull();
assertThat(property.setter()).isNotNull();
assertThatNoException().isThrownBy(() -> property.setter().invoke(instance, "Hello"));
assertThat(instance.str).isEqualTo("Hello");
})
.hasEntrySatisfying("bool", property -> {
assertThat(property.field().getName()).isEqualTo("bool");
assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class);
assertThat(property.type()).isEqualTo(Boolean.TYPE);
assertThat(property.isFinal()).isFalse();
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNotNull();
assertThatNoException()
.isThrownBy(() -> property.setter().invoke(instance, true));
assertThatNoException()
.isThrownBy(() -> assertThat((boolean) property.getter().invokeExact(instance)).isTrue());
})
.hasEntrySatisfying("boolObj", property -> {
assertThat(property.field().getName()).isEqualTo("boolObj");
assertThat(property.field().getDeclaringClass()).isEqualTo(ClassicPojo.class);
assertThat(property.type()).isEqualTo(Boolean.class);
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNotNull();
assertThatNoException()
.isThrownBy(() -> property.setter().invoke(instance, true));
assertThatNoException()
.isThrownBy(() -> assertThat((Boolean) property.getter().invokeExact(instance)).isTrue());
});
}
@Test
void propertiesFluidAndChainedAccessors() {
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(FluidChainedPojo.class);
FluidChainedPojo instance = new FluidChainedPojo(123);
Map<String, PojoClassIntrospector.Property> result = assertDoesNotThrow(introspector::properties);
assertThat(result).hasSize(3)
.hasEntrySatisfying("integer", property -> {
assertThat(property.field().getName()).isEqualTo("integer");
assertThat(property.field().getDeclaringClass()).isEqualTo(FluidChainedPojo.class);
assertThat(property.type()).isEqualTo(Integer.TYPE);
assertThat(property.isFinal()).isTrue();
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNull();
assertThatNoException()
.isThrownBy(() -> assertThat((int) property.getter().invokeExact(instance)).isEqualTo(123));
})
.hasEntrySatisfying("str", property -> {
assertThat(property.field().getName()).isEqualTo("str");
assertThat(property.field().getDeclaringClass()).isEqualTo(FluidChainedPojo.class);
assertThat(property.type()).isEqualTo(String.class);
assertThat(property.isFinal()).isFalse();
assertThat(property.getter()).isNull();
assertThat(property.setter()).isNotNull();
assertThatNoException().isThrownBy(() -> property.setter().invoke(instance, "Hello"));
assertThat(instance.str).isEqualTo("Hello");
})
.hasEntrySatisfying("bool", property -> {
assertThat(property.field().getName()).isEqualTo("bool");
assertThat(property.field().getDeclaringClass()).isEqualTo(FluidChainedPojo.class);
assertThat(property.type()).isEqualTo(Boolean.TYPE);
assertThat(property.isFinal()).isFalse();
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNotNull();
assertThatNoException()
.isThrownBy(() -> property.setter().invoke(instance, true));
assertThatNoException()
.isThrownBy(() -> assertThat((boolean) property.getter().invokeExact(instance)).isTrue());
});
}
@Test
void propertiesDirectAccess() {
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(DirectAccessPojo.class);
DirectAccessPojo instance = new DirectAccessPojo(123);
Map<String, PojoClassIntrospector.Property> result = assertDoesNotThrow(introspector::properties);
assertThat(result).hasSize(2)
.hasEntrySatisfying("integer", property -> {
assertThat(property.field().getName()).isEqualTo("integer");
assertThat(property.field().getDeclaringClass()).isEqualTo(DirectAccessPojo.class);
assertThat(property.type()).isEqualTo(Integer.TYPE);
assertThat(property.isFinal()).isTrue();
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNull();
assertThatNoException()
.isThrownBy(() -> assertThat(property.getter().invoke(instance)).isEqualTo(123));
})
.hasEntrySatisfying("str", property -> {
assertThat(property.field().getName()).isEqualTo("str");
assertThat(property.field().getDeclaringClass()).isEqualTo(DirectAccessPojo.class);
assertThat(property.type()).isEqualTo(String.class);
assertThat(property.isFinal()).isFalse();
assertThat(property.getter()).isNotNull();
assertThat(property.setter()).isNotNull();
assertThatNoException()
.isThrownBy(() -> property.setter().invoke(instance, "abcd"));
assertThat(instance.str).isEqualTo("abcd");
});
}
@Test
void noArgsConstructorNone() {
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(ClassicPojo.class);
assertThat(introspector.noArgsConstructor()).isNull();
}
@Test
void noArgsConstructor() {
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(NoArgs.class);
assertThat(introspector.noArgsConstructor())
.isNotNull()
.satisfies(constructor -> assertThat(constructor.invoke()).isInstanceOf(NoArgs.class));
}
@SuppressWarnings("unused")
@RequiredArgsConstructor
public static class ClassicPojo {
final int integer;
String str;
boolean bool;
Boolean boolObj;
public int getInteger() {
return integer;
}
public void setStr(String str) {
this.str = str;
}
public boolean isBool() {
return bool;
}
public void setBool(boolean bool) {
this.bool = bool;
}
public Boolean isBoolObj() {
return boolObj;
}
public void setBoolObj(Boolean boolObj) {
this.boolObj = boolObj;
}
}
@SuppressWarnings("unused")
@RequiredArgsConstructor
public static class FluidChainedPojo {
final int integer;
String str;
boolean bool;
public int integer() {
return integer;
}
public FluidChainedPojo str(String value) {
this.str = value;
return this;
}
public boolean bool() {
return bool;
}
public FluidChainedPojo bool(boolean value) {
this.bool = value;
return this;
}
}
@SuppressWarnings("unused")
@RequiredArgsConstructor
public static class DirectAccessPojo {
public final int integer;
public String str;
}
public static class NoArgs {
public String noop;
}
}

View File

@@ -0,0 +1,71 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving;
import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import lombok.Data;
import org.junit.jupiter.api.Test;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isCompoundEntryForClassWith;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("unused")
class TweedPojoWeaverBootstrapperTest {
@Test
void defaultWeaving() {
TweedPojoWeaverBootstrapper<DefaultWeaving> bootstrapper = TweedPojoWeaverBootstrapper.create(DefaultWeaving.class);
ConfigContainer<DefaultWeaving> configContainer = bootstrapper.weave();
assertThat(configContainer.rootEntry()).satisfies(isCompoundEntryForClassWith(DefaultWeaving.class, rootCompound ->
assertThat(rootCompound.subEntries())
.hasEntrySatisfying("primitiveInteger", isSimpleEntryForClass(int.class))
.hasEntrySatisfying("boxedDouble", isSimpleEntryForClass(Double.class))
.hasEntrySatisfying("value", isSimpleEntryForClass(InnerValue.class))
.hasEntrySatisfying("compound", isCompoundEntryForClassWith(InnerCompound.class, innerCompound ->
assertThat(innerCompound.subEntries())
.hasEntrySatisfying("string", isSimpleEntryForClass(String.class))
.hasSize(1)))
.hasSize(4)
));
configContainer.initialize();
assertThat(configContainer.extensions())
.satisfiesOnlyOnce(extension -> assertThat(extension).isInstanceOf(DummyExtension.class))
.hasSize(1);
}
@AutoService(DummyExtension.class)
public static class DummyExtension implements TweedExtension {
@Override
public String getId() {
return "dummy";
}
}
@PojoWeaving(extensions = {DummyExtension.class})
@CompoundWeaving(namingFormat = "camel_case")
@Data
public static class DefaultWeaving {
int primitiveInteger;
Double boxedDouble;
InnerValue value;
InnerCompound compound;
}
@CompoundWeaving
@Data
public static class InnerCompound {
String string;
}
@Data
public static class InnerValue {
int something;
boolean somethingElse;
}
}

View File

@@ -0,0 +1,32 @@
package de.siphalor.tweed5.weaver.pojo.test;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
public class ConfigEntryAssertions {
public static Consumer<Object> isSimpleEntryForClass(Class<?> valueClass) {
return object -> assertThat(object)
.asInstanceOf(type(SimpleConfigEntry.class))
.extracting(ConfigEntry::valueClass)
.isEqualTo(valueClass);
}
@SuppressWarnings("unchecked")
public static <T> Consumer<Object> isCompoundEntryForClassWith(
Class<T> compoundClass,
Consumer<CompoundConfigEntry<T>> condition
) {
return object -> assertThat(object)
.asInstanceOf(type(CompoundConfigEntry.class))
.satisfies(compoundEntry -> {
assertThat(compoundEntry.valueClass()).isEqualTo(compoundClass);
condition.accept(compoundEntry);
});
}
}