[weaver-pojo] Implement first prototype of POJO weaving
This commit is contained in:
6
tweed5-weaver-pojo/build.gradle.kts
Normal file
6
tweed5-weaver-pojo/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@ApiStatus.Internal
|
||||
|
||||
package de.siphalor.tweed5.weaver.pojo.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user