[*] Rework registration of TweedExtensions

This commit is contained in:
2025-04-25 15:33:55 +02:00
parent c97f711c0b
commit 59f882bd12
27 changed files with 639 additions and 257 deletions

View File

@@ -9,6 +9,7 @@ import org.jspecify.annotations.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
/**
* The main wrapper for a config tree.<br />
@@ -20,16 +21,14 @@ public interface ConfigContainer<T> {
@SuppressWarnings("rawtypes")
TweedConstructFactory<ConfigContainer> FACTORY = TweedConstructFactory.builder(ConfigContainer.class).build();
default void registerExtensions(TweedExtension... extensions) {
for (TweedExtension extension : extensions) {
registerExtension(extension);
default void registerExtensions(Class<? extends TweedExtension>... extensionClasses) {
for (Class<? extends TweedExtension> extensionClass : extensionClasses) {
registerExtension(extensionClass);
}
}
void registerExtension(Class<? extends TweedExtension> extensionClass);
void registerExtension(TweedExtension extension);
@Nullable
<E extends TweedExtension> E extension(Class<E> extensionClass);
<E extends TweedExtension> Optional<E> extension(Class<E> extensionClass);
Collection<TweedExtension> extensions();
void finishExtensionSetup();

View File

@@ -1,11 +1,18 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
public interface TweedExtension {
TweedConstructFactory<TweedExtension> FACTORY = TweedConstructFactory.builder(TweedExtension.class)
.typedArg(ConfigContainer.class)
.typedArg(TweedExtensionSetupContext.class)
.build();
String getId();
default void setup(TweedExtensionSetupContext context) {
default void extensionsFinalized() {
}
default void initEntry(ConfigEntry<?> configEntry) {

View File

@@ -1,9 +1,6 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
public interface TweedExtensionSetupContext {
ConfigContainer<?> configContainer();
<E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass);
void registerExtension(TweedExtension extension);
void registerExtension(Class<? extends TweedExtension> extensionClass);
}

View File

@@ -3,7 +3,10 @@ package de.siphalor.tweed5.core.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.*;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
@@ -12,56 +15,34 @@ import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import de.siphalor.tweed5.utils.api.collection.InheritanceMap;
import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
@NullUnmarked
public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final Set<Class<? extends TweedExtension>> requestedExtensions = new HashSet<>();
private final InheritanceMap<TweedExtension> extensions = new InheritanceMap<>(TweedExtension.class);
private ConfigEntry<T> rootEntry;
private PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass;
private Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions;
private @Nullable ConfigEntry<T> rootEntry;
private @Nullable PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass;
private final Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions
= new HashMap<>();
@Override
public <E extends TweedExtension> @Nullable E extension(Class<E> extensionClass) {
try {
return extensions.getSingleInstance(extensionClass);
} catch (InheritanceMap.NonUniqueResultException e) {
return null;
}
}
@Override
public Collection<TweedExtension> extensions() {
return Collections.unmodifiableCollection(extensions.values());
}
@Override
public void registerExtension(TweedExtension extension) {
public void registerExtension(Class<? extends TweedExtension> extensionClass) {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
if (!extensions.putIfAbsent(extension)) {
throw new IllegalArgumentException("Extension " + extension.getClass().getName() + " is already registered");
}
requestedExtensions.add(extensionClass);
}
@Override
public void finishExtensionSetup() {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
registeredEntryDataExtensions = new HashMap<>();
Collection<TweedExtension> additionalExtensions = new ArrayList<>();
TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() {
@Override
public ConfigContainer<T> configContainer() {
return DefaultConfigContainer.this;
}
@Override
public <E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass) {
if (registeredEntryDataExtensions.containsKey(dataClass)) {
@@ -73,24 +54,31 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
}
@Override
public void registerExtension(TweedExtension extension) {
if (!extensions.containsAnyInstanceForClass(extension.getClass())) {
additionalExtensions.add(extension);
public void registerExtension(Class<? extends TweedExtension> extensionClass) {
if (!extensions.containsAnyInstanceForClass(extensionClass)) {
requestedExtensions.add(extensionClass);
}
}
};
Collection<TweedExtension> extensionsToSetup = extensions.values();
while (!extensionsToSetup.isEmpty()) {
for (TweedExtension extension : extensionsToSetup) {
extension.setup(extensionSetupContext);
}
Set<Class<? extends TweedExtension>> abstractExtensionClasses = new HashSet<>();
for (TweedExtension additionalExtension : additionalExtensions) {
extensions.putIfAbsent(additionalExtension);
while (true) {
if (!requestedExtensions.isEmpty()) {
Class<? extends TweedExtension> extensionClass = popFromIterable(requestedExtensions);
if (isAbstractClass(extensionClass)) {
abstractExtensionClasses.add(extensionClass);
} else {
extensions.put(instantiateExtension(extensionClass, extensionSetupContext));
}
} else if (!abstractExtensionClasses.isEmpty()) {
Class<? extends TweedExtension> extensionClass = popFromIterable(abstractExtensionClasses);
if (!extensions.containsAnyInstanceForClass(extensionClass)) {
extensions.put(instantiateAbstractExtension(extensionClass, extensionSetupContext));
}
} else {
break;
}
extensionsToSetup = new ArrayList<>(additionalExtensions);
additionalExtensions.clear();
}
PatchworkClassCreator<EntryExtensionsData> entryExtensionsDataGenerator = PatchworkClassCreator.<EntryExtensionsData>builder()
@@ -109,16 +97,135 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
}
setupPhase = ConfigContainerSetupPhase.TREE_SETUP;
extensions.values().forEach(TweedExtension::extensionsFinalized);
}
private static <T> T popFromIterable(Iterable<T> iterable) {
Iterator<T> iterator = iterable.iterator();
T value = iterator.next();
iterator.remove();
return value;
}
protected <E extends TweedExtension> E instantiateAbstractExtension(
Class<E> extensionClass,
TweedExtensionSetupContext setupContext
) {
try {
Field defaultField = extensionClass.getDeclaredField("DEFAULT");
if (defaultField.getType() != Class.class) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field has incorrect class " + defaultField.getType().getName() + "."
));
}
if ((defaultField.getModifiers() & Modifier.STATIC) == 0) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field is not static."
));
}
if ((defaultField.getModifiers() & Modifier.PUBLIC) == 0) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field is not public."
));
}
Class<?> defaultClass = (Class<?>) defaultField.get(null);
if (!extensionClass.isAssignableFrom(defaultClass)) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"DEFAULT field contains class " + defaultClass.getName() + ", but that class "
+ "does not inherit from " + extensionClass.getName()
));
}
//noinspection unchecked
return instantiateExtension((Class<? extends E>) defaultClass, setupContext);
} catch (NoSuchFieldException ignored) {
throw new IllegalStateException(createAbstractExtensionInstantiationExceptionMessage(extensionClass, null));
} catch (IllegalAccessException e) {
throw new IllegalStateException(
createAbstractExtensionInstantiationExceptionMessage(
extensionClass,
"Couldn't access DEFAULT field."
),
e
);
}
}
private String createAbstractExtensionInstantiationExceptionMessage(
Class<?> extensionClass,
@Nullable String detail
) {
StringBuilder sb = new StringBuilder();
sb.append("Requested extension class ").append(extensionClass.getName()).append(" is ");
if (extensionClass.isInterface()) {
sb.append("an interface ");
} else {
sb.append("an abstract class ");
}
sb.append("and cannot be instantiated directly.\n");
sb.append("As the extension developer you can declare a public static DEFAULT field containing the class of ");
sb.append("a default implementation.\n");
sb.append("As a user you can try registering an implementation of the extension class directly.");
if (detail != null) {
sb.append("\n");
sb.append(detail);
}
return sb.toString();
}
protected <E extends TweedExtension> E instantiateExtension(
Class<E> extensionClass,
TweedExtensionSetupContext setupContext
) {
if (isAbstractClass(extensionClass)) {
throw new IllegalStateException(
"Cannot instantiate extension class " + extensionClass.getName() + " as it is abstract"
);
}
return TweedExtension.FACTORY.construct(extensionClass)
.typedArg(TweedExtensionSetupContext.class, setupContext)
.typedArg(ConfigContainer.class, this)
.finish();
}
private boolean isAbstractClass(Class<?> clazz) {
return clazz.isInterface() || (clazz.getModifiers() & Modifier.ABSTRACT) != 0;
}
@Override
public <E extends TweedExtension> Optional<E> extension(Class<E> extensionClass) {
requireSetupPhase(
ConfigContainerSetupPhase.TREE_SETUP,
ConfigContainerSetupPhase.TREE_SEALED,
ConfigContainerSetupPhase.READY
);
try {
return Optional.ofNullable(extensions.getSingleInstance(extensionClass));
} catch (InheritanceMap.NonUniqueResultException e) {
throw new IllegalStateException("Multiple extensions registered for class " + extensionClass.getName(), e);
}
}
@Override
public Collection<TweedExtension> extensions() {
requireSetupPhase(
ConfigContainerSetupPhase.TREE_SETUP,
ConfigContainerSetupPhase.TREE_SEALED,
ConfigContainerSetupPhase.READY
);
return Collections.unmodifiableCollection(extensions.values());
}
@Override
public void attachAndSealTree(ConfigEntry<T> rootEntry) {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
this.rootEntry = rootEntry;
finishEntrySetup();
}
private void finishEntrySetup() {
this.rootEntry = rootEntry;
rootEntry.visitInOrder(entry -> {
if (!entry.sealed()) {
entry.seal(DefaultConfigContainer.this);
@@ -133,6 +240,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
try {
assert entryExtensionsDataPatchworkClass != null;
return (EntryExtensionsData) entryExtensionsDataPatchworkClass.constructor().invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to construct patchwork class for entry extensions' data", e);
@@ -141,7 +249,11 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Override
public Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED);
requireSetupPhase(
ConfigContainerSetupPhase.TREE_SETUP,
ConfigContainerSetupPhase.TREE_SEALED,
ConfigContainerSetupPhase.READY
);
return registeredEntryDataExtensions;
}
@@ -149,6 +261,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
public void initialize() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED);
assert rootEntry != null;
rootEntry.visitInOrder(entry -> {
for (TweedExtension extension : extensions()) {
extension.initEntry(entry);
@@ -160,12 +273,32 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Override
public ConfigEntry<T> rootEntry() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SEALED, ConfigContainerSetupPhase.READY);
assert rootEntry != null;
return rootEntry;
}
private void requireSetupPhase(ConfigContainerSetupPhase required) {
if (setupPhase != required) {
throw new IllegalStateException("Config container is not in correct stage, expected " + required + ", but is in " + setupPhase);
private void requireSetupPhase(ConfigContainerSetupPhase... allowedPhases) {
for (ConfigContainerSetupPhase allowedPhase : allowedPhases) {
if (allowedPhase == setupPhase) {
return;
}
}
if (allowedPhases.length == 1) {
throw new IllegalStateException(
"Config container is not in correct phase, expected "
+ allowedPhases[0]
+ ", but is in "
+ setupPhase
);
} else {
throw new IllegalStateException(
"Config container is not in correct phase, expected any of "
+ Arrays.toString(allowedPhases)
+ ", but is in "
+ setupPhase
);
}
}

View File

@@ -0,0 +1,247 @@
package de.siphalor.tweed5.core.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import lombok.Getter;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class DefaultConfigContainerTest {
@Test
void extensionSetup() {
var configContainer = new DefaultConfigContainer<>();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
configContainer.registerExtension(ExtensionA.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SETUP);
assertThat(configContainer.extensions())
.satisfiesExactlyInAnyOrder(
extension -> assertThat(extension).isInstanceOf(ExtensionA.class),
extension -> assertThat(extension).isInstanceOf(ExtensionBImpl.class)
);
}
@Test
void extensionSetupDefaultOverride() {
var configContainer = new DefaultConfigContainer<>();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
configContainer.registerExtensions(ExtensionA.class, ExtensionBNonDefaultImpl.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SETUP);
assertThat(configContainer.extensions())
.satisfiesExactlyInAnyOrder(
extension -> assertThat(extension).isInstanceOf(ExtensionA.class),
extension -> assertThat(extension).isInstanceOf(ExtensionBNonDefaultImpl.class)
);
}
@Test
void extensionSetupMissingDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionMissingDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup).hasMessageContaining("ExtensionMissingDefault");
}
@Test
void extensionSetupAbstractDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionSelfReferencingDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup).hasMessageContaining("ExtensionSelfReferencingDefault");
}
@Test
void extensionSetupWrongDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionWrongDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup)
.hasMessageContaining("ExtensionWrongDefault", "ExtensionBImpl");
}
@Test
void extensionSetupNonStaticDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionNonStaticDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup)
.hasMessageContaining("ExtensionNonStaticDefault", "static");
}
@Test
void extensionSetupNonPublicDefault() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionNonPublicDefault.class);
assertThatThrownBy(configContainer::finishExtensionSetup)
.hasMessageContaining("ExtensionNonPublicDefault", "public");
}
@Test
void extension() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtensions(ExtensionA.class, ExtensionB.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.extension(ExtensionA.class)).containsInstanceOf(ExtensionA.class);
assertThat(configContainer.extension(ExtensionB.class)).containsInstanceOf(ExtensionBImpl.class);
assertThat(configContainer.extension(ExtensionBImpl.class)).containsInstanceOf(ExtensionBImpl.class);
assertThat(configContainer.extension(ExtensionBNonDefaultImpl.class)).isEmpty();
}
@SuppressWarnings("unchecked")
@Test
void attachAndSealTree() {
var configContainer = new DefaultConfigContainer<Map<String, Object>>();
var compoundEntry = new StaticMapCompoundConfigEntryImpl<>(
(Class<Map<String, Object>>)(Class<?>) Map.class,
(capacity) -> new HashMap<>(capacity * 2, 0.5F)
);
var subEntry = new SimpleConfigEntryImpl<>(String.class);
compoundEntry.addSubEntry("test", subEntry);
configContainer.finishExtensionSetup();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SETUP);
configContainer.attachAndSealTree(compoundEntry);
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SEALED);
assertThat(compoundEntry.sealed()).isTrue();
assertThat(subEntry.sealed()).isTrue();
assertThat(configContainer.rootEntry()).isSameAs(compoundEntry);
}
@Test
void createExtensionsData() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionB.class);
configContainer.finishExtensionSetup();
var extensionData = configContainer.createExtensionsData();
assertThat(extensionData).isNotNull()
.satisfies(
data -> assertThat(data.isPatchworkPartDefined(ExtensionBData.class)).isTrue(),
data -> assertThat(data.isPatchworkPartDefined(String.class)).isFalse()
);
}
@Test
void entryDataExtensions() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionB.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.entryDataExtensions()).containsOnlyKeys(ExtensionBData.class);
//noinspection unchecked
var registeredExtension = (RegisteredExtensionData<EntryExtensionsData, ExtensionBData>)
configContainer.entryDataExtensions().get(ExtensionBData.class);
var extensionsData = configContainer.createExtensionsData();
registeredExtension.set(extensionsData, new ExtensionBDataImpl("blub"));
assertThat(((ExtensionBData) extensionsData).test()).isEqualTo("blub");
}
@Test
void initialize() {
var configContainer = new DefaultConfigContainer<Map<String, Object>>();
var compoundEntry = new StaticMapCompoundConfigEntryImpl<>(
(Class<Map<String, Object>>)(Class<?>) Map.class,
(capacity) -> new HashMap<>(capacity * 2, 0.5F)
);
var subEntry = new SimpleConfigEntryImpl<>(String.class);
compoundEntry.addSubEntry("test", subEntry);
configContainer.registerExtension(ExtensionInitTracker.class);
configContainer.finishExtensionSetup();
configContainer.attachAndSealTree(compoundEntry);
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.TREE_SEALED);
configContainer.initialize();
assertThat(configContainer.setupPhase()).isEqualTo(ConfigContainerSetupPhase.READY);
var initTracker = configContainer.extension(ExtensionInitTracker.class).orElseThrow();
assertThat(initTracker.initializedEntries()).containsExactlyInAnyOrder(compoundEntry, subEntry);
}
public static class ExtensionA implements TweedExtension {
public ExtensionA(TweedExtensionSetupContext context) {
context.registerExtension(ExtensionB.class);
}
@Override
public String getId() {
return "a";
}
}
public interface ExtensionB extends TweedExtension {
Class<? extends ExtensionB> DEFAULT = ExtensionBImpl.class;
}
public static class ExtensionBImpl implements ExtensionB {
public ExtensionBImpl(TweedExtensionSetupContext context) {
context.registerEntryExtensionData(ExtensionBData.class);
}
@Override
public String getId() {
return "b";
}
}
public static class ExtensionBNonDefaultImpl implements ExtensionB {
@Override
public String getId() {
return "b-non-default";
}
}
public interface ExtensionBData {
String test();
}
record ExtensionBDataImpl(String test) implements ExtensionBData { }
@Getter
public static class ExtensionInitTracker implements TweedExtension {
private final Collection<ConfigEntry<?>> initializedEntries = new ArrayList<>();
@Override
public String getId() {
return "init-tracker";
}
@Override
public void initEntry(ConfigEntry<?> configEntry) {
initializedEntries.add(configEntry);
}
}
public interface ExtensionMissingDefault extends TweedExtension {}
public interface ExtensionSelfReferencingDefault extends TweedExtension {
Class<ExtensionSelfReferencingDefault> DEFAULT = ExtensionSelfReferencingDefault.class;
}
public interface ExtensionWrongDefault extends TweedExtension {
Class<?> DEFAULT = ExtensionBImpl.class;
}
public static abstract class ExtensionNonStaticDefault implements TweedExtension {
public final Class<?> DEFAULT = ExtensionNonStaticDefault.class;
}
public static abstract class ExtensionNonPublicDefault implements TweedExtension {
protected static final Class<?> DEFAULT = ExtensionNonPublicDefault.class;
}
}