[weaver-pojo] Coherent collection weaving

This commit is contained in:
2025-04-20 21:04:21 +02:00
parent e30e6d0547
commit a50ce563e6
12 changed files with 347 additions and 19 deletions

View File

@@ -0,0 +1,14 @@
package de.siphalor.tweed5.weaver.pojo.api.annotation;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCoherentCollectionConfigEntry;
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, ElementType.TYPE_USE})
public @interface CoherentCollectionWeaving {
Class<? extends WeavableCoherentCollectionConfigEntry> entryClass() default WeavableCoherentCollectionConfigEntry.class;
}

View File

@@ -0,0 +1,40 @@
package de.siphalor.tweed5.weaver.pojo.api.entry;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.function.IntFunction;
/**
* {@inheritDoc}
* <br />
* A constructor taking the value {@link Class}
* and a {@link java.util.function.IntFunction} that allows to instantiate the value class with a single capacity argument.
*/
public interface WeavableCoherentCollectionConfigEntry<E, T extends Collection<E>>
extends CoherentCollectionConfigEntry<E, T> {
static <E, T extends Collection<E>, C extends WeavableCoherentCollectionConfigEntry<E, T>> C instantiate(
Class<C> weavableClass, Class<T> valueClass, IntFunction<T> constructor
) throws PojoWeavingException {
try {
Constructor<C> weavableEntryConstructor = weavableClass.getConstructor(Class.class, IntFunction.class);
return weavableEntryConstructor.newInstance(valueClass, constructor);
} 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 collection entry " + weavableClass.getName(),
e
);
}
}
void elementEntry(ConfigEntry<E> elementEntry);
}

View File

@@ -0,0 +1,125 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CoherentCollectionWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCoherentCollectionConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.coherentcollection.CoherentCollectionWeavingConfig;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.coherentcollection.CoherentCollectionWeavingConfigImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.AnnotatedElement;
import java.util.*;
import java.util.function.IntFunction;
public class CoherentCollectionPojoWeaver implements TweedPojoWeaver {
private static final CoherentCollectionWeavingConfig DEFAULT_WEAVING_CONFIG = CoherentCollectionWeavingConfigImpl.builder()
.coherentCollectionEntryClass(de.siphalor.tweed5.weaver.pojo.impl.entry.CoherentCollectionConfigEntryImpl.class)
.build();
private RegisteredExtensionData<WeavingContext.ExtensionsData, CoherentCollectionWeavingConfig> weavingConfigAccess;
@Override
public void setup(SetupContext context) {
this.weavingConfigAccess = context.registerWeavingContextExtensionData(CoherentCollectionWeavingConfig.class);
}
@Override
public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
List<ActualType<?>> collectionTypeParams = valueType.getTypesOfSuperArguments(Collection.class);
if (collectionTypeParams == null) {
return null;
}
try {
CoherentCollectionWeavingConfig weavingConfig = getOrCreateWeavingConfig(context);
WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy();
weavingConfigAccess.set(newExtensionsData, weavingConfig);
IntFunction<Collection<Object>> constructor = getCollectionConstructor(valueType);
//noinspection unchecked,rawtypes
WeavableCoherentCollectionConfigEntry configEntry = WeavableCoherentCollectionConfigEntry.instantiate(
(Class) weavingConfig.coherentCollectionEntryClass(),
(Class) valueType.declaredType(),
constructor
);
configEntry.elementEntry(context.weaveEntry(
collectionTypeParams.get(0),
context.subContextBuilder("element")
.annotations(collectionTypeParams.get(0))
.extensionsData(newExtensionsData)
.build()
));
configEntry.seal(context.configContainer());
return configEntry;
} catch (Exception e) {
throw new PojoWeavingException("Exception occurred trying to weave collectoin for class " + valueType, e);
}
}
private CoherentCollectionWeavingConfig getOrCreateWeavingConfig(WeavingContext context) {
CoherentCollectionWeavingConfig parent;
if (context.extensionsData().isPatchworkPartSet(CoherentCollectionWeavingConfig.class)) {
parent = (CoherentCollectionWeavingConfig) context.extensionsData();
} else {
parent = DEFAULT_WEAVING_CONFIG;
}
CoherentCollectionWeavingConfig local = createWeavingConfigFromAnnotations(context.annotations());
if (local == null) {
return parent;
}
return CoherentCollectionWeavingConfigImpl.withOverrides(parent, local);
}
private CoherentCollectionWeavingConfig createWeavingConfigFromAnnotations(@NotNull AnnotatedElement annotations) {
CoherentCollectionWeaving annotation = annotations.getAnnotation(CoherentCollectionWeaving.class);
if (annotation == null) {
return null;
}
CoherentCollectionWeavingConfigImpl.CoherentCollectionWeavingConfigImplBuilder builder = CoherentCollectionWeavingConfigImpl.builder();
if (annotation.entryClass() != null) {
builder.coherentCollectionEntryClass(annotation.entryClass());
}
return builder.build();
}
public IntFunction<Collection<Object>> getCollectionConstructor(ActualType<?> type) {
if (type.declaredType() == List.class) {
return ArrayList::new;
} else if (type.declaredType() == Set.class) {
return capacity -> new HashSet<>((int) Math.ceil(capacity * 1.4), 0.75F);
}
try {
return findCompatibleConstructor(type);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new PojoWeavingException("could not find no args constructor for " + type, e);
}
}
public IntFunction<Collection<Object>> findCompatibleConstructor(ActualType<?> type) throws
NoSuchMethodException,
IllegalAccessException {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle constructor = lookup.findConstructor(type.declaredType(), MethodType.methodType(Void.class));
return capacity -> {
try {
//noinspection unchecked
return (Collection<Object>) constructor.invoke();
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
}

View File

@@ -63,6 +63,8 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
}
});
compoundEntry.seal(context.configContainer());
return compoundEntry;
} catch (Exception e) {
throw new PojoWeavingException("Exception occurred trying to weave compound for class " + valueType, e);

View File

@@ -13,6 +13,8 @@ public class TrivialPojoWeaver implements TweedPojoWeaver {
@Override
public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
return new SimpleConfigEntryImpl<>(valueType.declaredType());
SimpleConfigEntryImpl<T> entry = new SimpleConfigEntryImpl<>(valueType.declaredType());
entry.seal(context.configContainer());
return entry;
}
}

View File

@@ -36,7 +36,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
return new Builder(null, weavingFunction, configContainer, new String[]{ baseName });
}
public Builder subContextBuilder(String subPathName) {
public Builder subContextBuilder(@NotNull String subPathName) {
String[] newPath = Arrays.copyOf(path, path.length + 1);
newPath[path.length] = subPathName;
return new Builder(this, weavingFunction, configContainer, newPath).extensionsData(extensionsData);

View File

@@ -0,0 +1,68 @@
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.WeavableCoherentCollectionConfigEntry;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.function.IntFunction;
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CoherentCollectionConfigEntryImpl<E, T extends Collection<E>> extends BaseConfigEntry<T> implements WeavableCoherentCollectionConfigEntry<E, T> {
private final IntFunction<T> constructor;
private ConfigEntry<E> elementEntry;
public CoherentCollectionConfigEntryImpl(@NotNull Class<T> valueClass, IntFunction<T> constructor) {
super(valueClass);
this.constructor = constructor;
}
@Override
public void elementEntry(ConfigEntry<E> elementEntry) {
this.elementEntry = elementEntry;
}
@Override
public T instantiateCollection(int size) {
try {
return constructor.apply(size);
} catch (Throwable e) {
throw new IllegalStateException("Failed to instantiate collection class", e);
}
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterCollectionEntry(this)) {
elementEntry.visitInOrder(visitor);
visitor.leaveCollectionEntry(this);
}
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterCollectionEntry(this, value)) {
for (E element : value) {
elementEntry.visitInOrder(visitor, element);
}
visitor.leaveCollectionEntry(this, value);
}
}
@Override
public @NotNull T deepCopy(@NotNull T value) {
T copy = instantiateCollection(value.size());
for (E element : value) {
copy.add(elementEntry.deepCopy(element));
}
return copy;
}
}

View File

@@ -0,0 +1,10 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.coherentcollection;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCoherentCollectionConfigEntry;
import org.jetbrains.annotations.Nullable;
public interface CoherentCollectionWeavingConfig {
@SuppressWarnings("rawtypes")
@Nullable
Class<? extends WeavableCoherentCollectionConfigEntry> coherentCollectionEntryClass();
}

View File

@@ -0,0 +1,21 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.coherentcollection;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCoherentCollectionConfigEntry;
import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.Nullable;
@Builder
@Value
public class CoherentCollectionWeavingConfigImpl implements CoherentCollectionWeavingConfig {
@SuppressWarnings("rawtypes")
@Nullable
Class<? extends WeavableCoherentCollectionConfigEntry> coherentCollectionEntryClass;
public static CoherentCollectionWeavingConfigImpl withOverrides(CoherentCollectionWeavingConfig self, CoherentCollectionWeavingConfig overrides) {
return CoherentCollectionWeavingConfigImpl.builder()
.coherentCollectionEntryClass(overrides.coherentCollectionEntryClass() != null ? overrides.coherentCollectionEntryClass() : self.coherentCollectionEntryClass())
.build();
}
}

View File

@@ -9,7 +9,6 @@ import org.jetbrains.annotations.Nullable;
@Builder
@Value
public class CompoundWeavingConfigImpl implements CompoundWeavingConfig {
private static final CompoundWeavingConfigImpl EMPTY = CompoundWeavingConfigImpl.builder().build();
NamingFormat compoundSourceNamingFormat;
NamingFormat compoundTargetNamingFormat;

View File

@@ -5,30 +5,35 @@ 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 de.siphalor.tweed5.weaver.pojo.api.weaving.CoherentCollectionPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.CompoundPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TrivialPojoWeaver;
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 java.util.List;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.*;
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();
TweedPojoWeaverBootstrapper<MainCompound> bootstrapper = TweedPojoWeaverBootstrapper.create(MainCompound.class);
ConfigContainer<MainCompound> 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)
assertThat(configContainer.rootEntry()).satisfies(isCompoundEntryForClassWith(MainCompound.class, rootCompound ->
assertThat(rootCompound.subEntries())
.hasEntrySatisfying("primitiveInteger", isSimpleEntryForClass(int.class))
.hasEntrySatisfying("boxedDouble", isSimpleEntryForClass(Double.class))
.hasEntrySatisfying("value", isSimpleEntryForClass(InnerValue.class))
.hasEntrySatisfying("list", isSimpleEntryForClass(List.class))
.hasEntrySatisfying("compound", isCompoundEntryForClassWith(InnerCompound.class, innerCompound ->
assertThat(innerCompound.subEntries())
.hasEntrySatisfying("string", isSimpleEntryForClass(String.class))
.hasSize(1)))
.hasSize(5)
));
configContainer.initialize();
@@ -38,6 +43,21 @@ class TweedPojoWeaverBootstrapperTest {
.hasSize(1);
}
@Test
void weavingWithList() {
TweedPojoWeaverBootstrapper<CompoundWithList> bootstrapper = TweedPojoWeaverBootstrapper.create(CompoundWithList.class);
ConfigContainer<CompoundWithList> configContainer = bootstrapper.weave();
assertThat(configContainer.rootEntry()).satisfies(isCompoundEntryForClassWith(CompoundWithList.class, rootCompound ->
assertThat(rootCompound.subEntries())
.hasEntrySatisfying("strings", isCollectionEntryForClass(
List.class,
list -> assertThat(list.elementEntry()).satisfies(isSimpleEntryForClass(String.class))
))
));
}
@AutoService(DummyExtension.class)
public static class DummyExtension implements TweedExtension {
@Override
@@ -49,10 +69,11 @@ class TweedPojoWeaverBootstrapperTest {
@PojoWeaving(extensions = {DummyExtension.class})
@CompoundWeaving(namingFormat = "camel_case")
@Data
public static class DefaultWeaving {
public static class MainCompound {
int primitiveInteger;
Double boxedDouble;
InnerValue value;
List<Integer> list;
InnerCompound compound;
}
@@ -68,4 +89,11 @@ class TweedPojoWeaverBootstrapperTest {
int something;
boolean somethingElse;
}
@PojoWeaving(weavers = {CompoundPojoWeaver.class, CoherentCollectionPojoWeaver.class, TrivialPojoWeaver.class})
@CompoundWeaving(namingFormat = "camel_case")
@Data
public static class CompoundWithList {
List<String> strings;
}
}

View File

@@ -1,9 +1,12 @@
package de.siphalor.tweed5.weaver.pojo.test;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
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 de.siphalor.tweed5.weaver.pojo.api.weaving.CoherentCollectionPojoWeaver;
import java.util.Collection;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
@@ -33,4 +36,20 @@ public class ConfigEntryAssertions {
condition::accept
);
}
public static Consumer<Object> isCollectionEntryForClass(
Class<?> collectionClass,
Consumer<CoherentCollectionConfigEntry<?, ?>> condition
) {
return object -> assertThat(object)
.as("Should be a collection config entry for class " + collectionClass.getName())
.asInstanceOf(type(CoherentCollectionConfigEntry.class))
.as("Collection entry for class " + collectionClass.getSimpleName())
.satisfies(
listEntry -> assertThat(listEntry.valueClass())
.as("Value class of collection entry should match")
.isEqualTo(collectionClass),
condition::accept
);
}
}