@NotNull Construct typedArg(@NotNull A value);
+
+ /**
+ * Binds a value to a typed argument of the given type.
+ *
+ * This allows binding the value to super classes of the value.
+ * @see #typedArg(Object)
+ * @see #namedArg(String, Object)
+ */
+ @Contract(mutates = "this", value = "_, _ -> this")
+ @NotNull Construct typedArg(@NotNull Class super A> argType, @Nullable A value);
+
+ /**
+ * Binds a value to a named argument.
+ * @see #typedArg(Object)
+ */
+ @Contract(mutates = "this", value = "_, _ -> this")
+ @NotNull Construct namedArg(@NotNull String name, @Nullable A value);
+
+ /**
+ * Finishes the binding and actually constructs the class.
+ */
+ @Contract(pure = true)
+ @NotNull C finish();
+ }
+}
diff --git a/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/Entry.java b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/Entry.java
new file mode 100644
index 0000000..a0ef694
--- /dev/null
+++ b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/Entry.java
@@ -0,0 +1,9 @@
+package de.siphalor.tweed5.construct.impl;
+
+import lombok.Value;
+
+@Value
+class Entry {
+ K key;
+ V value;
+}
diff --git a/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/TweedConstructFactoryImpl.java b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/TweedConstructFactoryImpl.java
new file mode 100644
index 0000000..725f75e
--- /dev/null
+++ b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/TweedConstructFactoryImpl.java
@@ -0,0 +1,464 @@
+package de.siphalor.tweed5.construct.impl;
+
+import de.siphalor.tweed5.construct.api.ConstructParameter;
+import de.siphalor.tweed5.construct.api.TweedConstruct;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
+import lombok.*;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.WrongMethodTypeException;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RequiredArgsConstructor
+@Getter(AccessLevel.PACKAGE)
+public class TweedConstructFactoryImpl implements TweedConstructFactory {
+ private static final int CONSTRUCTOR_MODIFIERS = Modifier.PUBLIC;
+ private static final int STATIC_METHOD_MODIFIERS = Modifier.PUBLIC | Modifier.STATIC;
+
+ private final Class constructBaseClass;
+ private final Set> typedArgs;
+ private final Map> namedArgs;
+ private final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
+ private final Map, Optional>> cachedConstructTargets = new HashMap<>();
+ @SuppressWarnings("unused")
+ private final ReadWriteLock cachedConstructTargetsLock = new ReentrantReadWriteLock();
+
+ public static TweedConstructFactoryImpl.FactoryBuilder builder(Class baseClass) {
+ return new FactoryBuilder<>(baseClass);
+ }
+
+ @Override
+ public TweedConstructFactory.@NotNull Construct construct(@NotNull Class subClass) {
+ return new Construct<>(getConstructTarget(subClass));
+ }
+
+ private @NotNull ConstructTarget getConstructTarget(Class type) {
+ ConstructTarget cachedConstructTarget = readConstructTargetFromCache(type);
+ if (cachedConstructTarget != null) {
+ return cachedConstructTarget;
+ }
+ ConstructTarget constructTarget = locateConstructTarget(type);
+ cacheConstructTarget(type, constructTarget);
+ return constructTarget;
+ }
+
+ @Locked.Read("cachedConstructTargetsLock")
+ private @Nullable ConstructTarget readConstructTargetFromCache(Class type) {
+ Optional> cachedConstructTarget = cachedConstructTargets.get(type);
+ if (cachedConstructTarget != null) {
+ if (!cachedConstructTarget.isPresent()) {
+ throw new IllegalStateException("Could not locate construct for " + type.getName());
+ } else {
+ //noinspection unchecked
+ return (ConstructTarget) cachedConstructTarget.get();
+ }
+ }
+ return null;
+ }
+
+ @Locked.Write("cachedConstructTargetsLock")
+ private void cacheConstructTarget(Class type, ConstructTarget constructTarget) {
+ cachedConstructTargets.put(type, Optional.of(constructTarget));
+ }
+
+ private ConstructTarget locateConstructTarget(Class type) {
+ if (!constructBaseClass.isAssignableFrom(type)) {
+ throw new IllegalArgumentException(
+ "Type " + type.getName() + " is not a subclass of " + constructBaseClass.getName()
+ );
+ }
+
+ Collection> constructorCandidates = findConstructorCandidates(type);
+ Collection staticConstructorCandidates = findStaticConstructorCandidates(type);
+
+ List annotated = Stream.concat(constructorCandidates.stream(), staticConstructorCandidates.stream())
+ .filter(candidate -> {
+ TweedConstruct annotation = candidate.getAnnotation(TweedConstruct.class);
+ return annotation != null && annotation.value().equals(constructBaseClass);
+ })
+ .collect(Collectors.toList());
+
+ if (annotated.size() > 1) {
+ throw new IllegalStateException(
+ "Found multiple matching constructors for " + type.getName()
+ + " annotated with a matching TweedConstruct for " + constructBaseClass.getName() + ": "
+ + annotated
+ );
+ } else if (annotated.size() == 1) {
+ return resolveConstructTarget(type, annotated.get(0));
+ } else if (constructorCandidates.size() == 1) {
+ return resolveConstructTarget(type, constructorCandidates.iterator().next());
+ } else {
+ throw new IllegalStateException(
+ "Failed to determine actual constructor on " + type.getName()
+ + " for " + constructBaseClass.getName() + ". "
+ + "Constructor candidates: " + constructorCandidates + "; "
+ + "Static method candidates: " + staticConstructorCandidates + ". "
+ + "The desired constructor should be marked with @" + TweedConstruct.class.getName()
+ );
+ }
+ }
+
+ private Collection> findConstructorCandidates(Class> type) {
+ return Arrays.stream(type.getConstructors())
+ .filter(constructor -> (constructor.getModifiers() & CONSTRUCTOR_MODIFIERS) == CONSTRUCTOR_MODIFIERS)
+ .collect(Collectors.toList());
+ }
+
+ private Collection findStaticConstructorCandidates(Class> type) {
+ return Arrays.stream(type.getDeclaredMethods())
+ .filter(method -> (method.getModifiers() & STATIC_METHOD_MODIFIERS) == STATIC_METHOD_MODIFIERS)
+ .filter(method -> type.isAssignableFrom(method.getReturnType()))
+ .collect(Collectors.toList());
+ }
+
+ private ConstructTarget resolveConstructTarget(Class type, Executable executable) {
+ Object[] argOrder = new Object[executable.getParameterCount()];
+
+ Map, List> typedParameters = new HashMap<>();
+ Map> namedParameters = new HashMap<>();
+ Parameter[] parameters = executable.getParameters();
+ boolean issue = false;
+ for (int i = 0; i < parameters.length; i++) {
+ Parameter parameter = parameters[i];
+ ConstructParameter annotation = parameter.getAnnotation(ConstructParameter.class);
+ if (annotation != null) {
+ String name = annotation.name();
+ List named = namedParameters.computeIfAbsent(name, n -> new ArrayList<>());
+ named.add(parameter);
+ Class> argType = namedArgs.get(name);
+ argOrder[i] = name;
+ if (!issue && (
+ named.size() > 1
+ || argType == null
+ || !boxClass(parameter.getType()).isAssignableFrom(argType)
+ )) {
+ issue = true;
+ }
+ } else {
+ Class> paramType = boxClass(parameter.getType());
+ List typed = typedParameters.computeIfAbsent(paramType, n -> new ArrayList<>());
+ typed.add(parameter);
+ argOrder[i] = paramType;
+ if (!issue && (typed.size() > 1 || !typedArgs.contains(paramType))) {
+ issue = true;
+ }
+ }
+ }
+
+ if (issue) {
+ throw new IllegalStateException(
+ createConstructorTargetArgCheckFailMessage(executable, typedParameters, namedParameters)
+ );
+ }
+
+ return new ConstructTarget<>(type, argOrder, createInvokerFromCandidate(type, executable));
+ }
+
+ private Function createInvokerFromCandidate(Class type, Executable executable) {
+ MethodHandle handle;
+ try {
+ if (executable instanceof Method) {
+ handle = lookup.unreflect((Method) executable);
+ } else if (executable instanceof Constructor) {
+ handle = lookup.unreflectConstructor((Constructor>) executable);
+ } else {
+ throw new IllegalStateException("Unsupported executable type: " + executable);
+ }
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Constructor for type " + type.getName() + " is not accessible", e);
+ }
+ return args -> {
+ try {
+ //noinspection unchecked
+ return (C) handle.invokeWithArguments(args);
+ } catch (ClassCastException | WrongMethodTypeException e) {
+ throw new IllegalStateException(
+ "Failed to construct type " + type.getName() + " as " + constructBaseClass.getName(), e
+ );
+ } catch (Throwable e) {
+ throw new RuntimeException(
+ "Uncaught exception during construct of type " + type.getName()
+ + " as " + constructBaseClass.getName(),
+ e
+ );
+ }
+ };
+ }
+
+ private String createConstructorTargetArgCheckFailMessage(
+ Executable executable,
+ Map, List> typedParameters,
+ Map> namedParameters
+ ) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Failed to resolve parameters for ");
+ sb.append(executable);
+ sb.append(" (for ");
+ sb.append(constructBaseClass.getName());
+ sb.append("). The following issues have been detected:");
+
+ Set> unexpectedTypes = new HashSet<>(typedParameters.keySet());
+ unexpectedTypes.removeAll(typedArgs);
+ if (!unexpectedTypes.isEmpty()) {
+ for (Class> unexpectedType : unexpectedTypes) {
+ sb.append("\n - Typed parameter of type ");
+ sb.append(unexpectedType.getName());
+ sb.append(" is not known: ");
+ sb.append(typedParameters.get(unexpectedType));
+ }
+ }
+ Set unexpectedNames = new HashSet<>(namedParameters.keySet());
+ unexpectedNames.removeAll(namedArgs.keySet());
+ if (!unexpectedNames.isEmpty()) {
+ for (String unexpectedName : unexpectedNames) {
+ sb.append("\n - Named parameter ");
+ sb.append(unexpectedName);
+ sb.append(" is not known: ");
+ sb.append(namedParameters.get(unexpectedName));
+ }
+ }
+
+ typedParameters.entrySet().stream()
+ .filter(entry -> entry.getValue().size() > 1)
+ .forEach(entry -> sb.append("\n - Duplicate typed parameter ")
+ .append(entry.getKey())
+ .append(": ")
+ .append(entry.getValue()));
+ namedParameters.entrySet().stream()
+ .filter(entry -> entry.getValue().size() > 1)
+ .forEach(entry -> sb.append("\n - Duplicate named parameter ")
+ .append(entry.getKey())
+ .append(": ")
+ .append(entry.getValue()));
+
+ namedParameters.entrySet().stream()
+ .filter(entry -> !unexpectedNames.contains(entry.getKey()))
+ .flatMap(entry -> entry.getValue().stream().map(parameter -> new Entry<>(entry.getKey(), parameter)))
+ .forEach(entry -> {
+ Class> argType = namedArgs.get(entry.key());
+ if (!boxClass(entry.value().getType()).isAssignableFrom(argType)) {
+ sb.append("\n - Named parameter ").append(entry.key());
+ sb.append(" expects values of type ").append(argType.getName());
+ sb.append(": ").append(entry.value());
+ }
+ });
+
+ return sb.toString();
+ }
+
+ @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+ public static class FactoryBuilder implements TweedConstructFactory.FactoryBuilder {
+ private final Class constructBaseClass;
+ private final Set> typedArgs = new HashSet<>();
+ private final Map> namedArgs = new HashMap<>();
+
+ @Override
+ public TweedConstructFactory.@NotNull FactoryBuilder typedArg(@NotNull Class argType) {
+ argType = boxClass(argType);
+ if (typedArgs.contains(argType)) {
+ throw new IllegalArgumentException("Argument for type " + argType + " has already been registered");
+ }
+ typedArgs.add(argType);
+ return this;
+ }
+
+ @Override
+ public TweedConstructFactory.@NotNull FactoryBuilder namedArg(
+ @NotNull String name,
+ @NotNull Class argType
+ ) {
+ Class> existingArgType = namedArgs.get(name);
+ if (existingArgType != null) {
+ throw new IllegalArgumentException(
+ "Argument for name " + name + " has already been registered; "
+ + "existing type " + existingArgType.getName() + "; "
+ + "new type " + argType.getName()
+ );
+ }
+ namedArgs.put(name, boxClass(argType));
+ return this;
+ }
+
+ @Override
+ public @NotNull TweedConstructFactory build() {
+ return new TweedConstructFactoryImpl<>(
+ constructBaseClass,
+ typedArgs,
+ namedArgs
+ );
+ }
+ }
+
+ @RequiredArgsConstructor
+ private class Construct implements TweedConstructFactory.Construct {
+ private final ConstructTarget target;
+ private final Map, Object> typedArgValues = new HashMap<>();
+ private final Map namedArgValues = new HashMap<>();
+
+ @Override
+ public TweedConstructFactory.@NotNull Construct typedArg(@NotNull A value) {
+ requireTypedArgExists(value.getClass(), value);
+ typedArgValues.put(value.getClass(), value);
+ return this;
+ }
+
+ @Override
+ public TweedConstructFactory.@NotNull Construct typedArg(@NotNull Class super A> argType, @Nullable A value) {
+ argType = boxClass(argType);
+ if (value != null && !argType.isAssignableFrom(value.getClass())) {
+ throw new IllegalArgumentException(
+ "Typed argument for type " + argType.getName()
+ + " is of incorrect type " + value.getClass().getName()
+ + ", value: " + value
+ );
+ }
+ requireTypedArgExists(argType, value);
+ typedArgValues.put(argType, value);
+ return this;
+ }
+
+ private void requireTypedArgExists(@NotNull Class> type, @Nullable A value) {
+ if (!typedArgs.contains(type)) {
+ throw new IllegalArgumentException(
+ "Typed argument for type " + type.getName() + " does not exist, value: " + value
+ );
+ }
+ }
+
+ @Override
+ public TweedConstructFactory.@NotNull Construct namedArg(@NotNull String name, @Nullable A value) {
+ Class> argType = namedArgs.get(name);
+ if (argType == null) {
+ throw new IllegalArgumentException(
+ "Named argument for name " + name + " does not exist, value: " + value
+ );
+ } else if (value != null && !argType.isAssignableFrom(value.getClass())) {
+ throw new IllegalArgumentException(
+ "Named argument for name " + name + " is defined with type " + argType.getName() +
+ " but got type " + value.getClass().getName() + " with value " + value
+ );
+ }
+ namedArgValues.put(name, value);
+ return this;
+ }
+
+ @Override
+ public @NotNull C finish() {
+ checkAllArgsFilled();
+
+ Object[] argValues = new Object[target.argOrder.length];
+ for (int i = 0; i < target.argOrder.length; i++) {
+ Object arg = target.argOrder[i];
+ if (arg instanceof Class>) {
+ argValues[i] = typedArgValues.get((Class>) arg);
+ } else if (arg instanceof String) {
+ argValues[i] = namedArgValues.get((String) arg);
+ } else {
+ throw new IllegalStateException("Encountered illegal argument indicator " + arg + " at " + i);
+ }
+ }
+ return target.invoker.apply(argValues);
+ }
+
+ private void checkAllArgsFilled() {
+ Set> missingTypedArgs = Collections.emptySet();
+ if (typedArgValues.size() != typedArgs.size()) {
+ missingTypedArgs = new HashSet<>(typedArgs);
+ missingTypedArgs.removeAll(typedArgValues.keySet());
+ }
+ Set missingNamedArgs = Collections.emptySet();
+ if (namedArgValues.size() != namedArgs.size()) {
+ missingNamedArgs = new HashSet<>(namedArgs.keySet());
+ missingNamedArgs.removeAll(namedArgValues.keySet());
+ }
+
+ if (!missingTypedArgs.isEmpty() || !missingNamedArgs.isEmpty()) {
+ throw new IllegalArgumentException(createMissingArgsMessage(missingTypedArgs, missingNamedArgs));
+ }
+ }
+
+ private String createMissingArgsMessage(Set> missingTypedArgs, Set missingNamedArgs) {
+ StringBuilder message = new StringBuilder()
+ .append("Missing arguments for construction of ")
+ .append(target.type().getName())
+ .append(" as ")
+ .append(constructBaseClass.getName())
+ .append(", missing: ");
+
+ if (!missingTypedArgs.isEmpty()) {
+ message.append("typed args (");
+ boolean requiresDelimiter = false;
+ for (Class> missingTypedArg : missingTypedArgs) {
+ if (requiresDelimiter) {
+ message.append(", ");
+ }
+ message.append(missingTypedArg.getName());
+ requiresDelimiter = true;
+ }
+ message.append(") ");
+ }
+ if (!missingNamedArgs.isEmpty()) {
+ message.append("named args (");
+ boolean requiresDelimiter = false;
+ for (String missingNamedArg : missingNamedArgs) {
+ if (requiresDelimiter) {
+ message.append(", ");
+ }
+ message.append(missingNamedArg);
+ requiresDelimiter = true;
+ }
+ message.append(") ");
+ }
+ return message.toString();
+ }
+ }
+
+ /**
+ * Boxes primitive classes into their reference variants.
+ * Allows for easier class comparison down the line.
+ */
+ @SuppressWarnings("unchecked")
+ static Class boxClass(Class type) {
+ if (!type.isPrimitive()) {
+ return type;
+ }
+ if (type == boolean.class) {
+ return (Class) Boolean.class;
+ } else if (type == byte.class) {
+ return (Class) Byte.class;
+ } else if (type == char.class) {
+ return (Class) Character.class;
+ } else if (type == short.class) {
+ return (Class) Short.class;
+ } else if (type == int.class) {
+ return (Class) Integer.class;
+ } else if (type == long.class) {
+ return (Class) Long.class;
+ } else if (type == float.class) {
+ return (Class) Float.class;
+ } else if (type == double.class) {
+ return (Class) Double.class;
+ } else if (type == void.class) {
+ return (Class) Void.class;
+ } else {
+ throw new IllegalArgumentException("Unsupported primitive type " + type);
+ }
+ }
+
+ @Value
+ private static class ConstructTarget {
+ Class> type;
+ Object[] argOrder;
+ Function invoker;
+ }
+}
diff --git a/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/package-info.java b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/package-info.java
new file mode 100644
index 0000000..2515b80
--- /dev/null
+++ b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/impl/package-info.java
@@ -0,0 +1,4 @@
+@ApiStatus.Internal
+package de.siphalor.tweed5.construct.impl;
+
+import org.jetbrains.annotations.ApiStatus;
diff --git a/tweed5-construct/src/test/java/de/siphalor/tweed5/construct/impl/TweedConstructFactoryImplTest.java b/tweed5-construct/src/test/java/de/siphalor/tweed5/construct/impl/TweedConstructFactoryImplTest.java
new file mode 100644
index 0000000..b1c027b
--- /dev/null
+++ b/tweed5-construct/src/test/java/de/siphalor/tweed5/construct/impl/TweedConstructFactoryImplTest.java
@@ -0,0 +1,483 @@
+package de.siphalor.tweed5.construct.impl;
+
+import de.siphalor.tweed5.construct.api.ConstructParameter;
+import de.siphalor.tweed5.construct.api.TweedConstruct;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
+import lombok.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.InstanceOfAssertFactories.type;
+
+@SuppressWarnings("unused")
+class TweedConstructFactoryImplTest {
+ @SuppressWarnings("unchecked")
+ @Test
+ void factoryBuilder() {
+ val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
+ builder.typedArg(Integer.class).typedArg(String.class);
+ builder.namedArg("hey", String.class).namedArg("ho", String.class);
+ TweedConstructFactory factory = builder.build();
+ assertThat(factory)
+ .asInstanceOf(type(TweedConstructFactoryImpl.class))
+ .satisfies(
+ f -> assertThat(f.constructBaseClass()).isEqualTo(DummyBase.class),
+ f -> assertThat(f.typedArgs())
+ .containsExactlyInAnyOrder(Integer.class, String.class),
+ f -> assertThat(f.namedArgs())
+ .containsEntry("hey", String.class)
+ .containsEntry("ho", String.class)
+ .hasSize(2)
+ );
+
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void factoryBuilderPrimitives() {
+ val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
+ builder.typedArg(int.class).typedArg(long.class);
+ builder.namedArg("bool", boolean.class).namedArg("byte", byte.class);
+ TweedConstructFactory factory = builder.build();
+ assertThat(factory)
+ .asInstanceOf(type(TweedConstructFactoryImpl.class))
+ .satisfies(
+ f -> assertThat(f.typedArgs())
+ .containsExactlyInAnyOrder(Integer.class, Long.class),
+ f -> assertThat(f.namedArgs())
+ .containsEntry("bool", Boolean.class)
+ .containsEntry("byte", Byte.class)
+ .hasSize(2)
+ );
+ }
+
+ @Test
+ void factoryBuilderDuplicateTypedArgs() {
+ val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
+ assertThatThrownBy(() -> {
+ builder.typedArg(Integer.class).typedArg(String.class).typedArg(Integer.class);
+ builder.build();
+ }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Integer");
+ }
+
+ @Test
+ void factoryBuilderDuplicateNamedArgs() {
+ val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
+ assertThatThrownBy(() -> {
+ builder.namedArg("hey", String.class).namedArg("ho", String.class).namedArg("hey", Integer.class);
+ builder.build();
+ }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("hey");
+ }
+
+ @Test
+ void constructMissingInheritance() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
+ //noinspection unchecked,RedundantCast
+ assertThatThrownBy(() ->
+ factory.construct((Class extends DummyBase>) (Class>) MissingInheritance.class)
+ ).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(DummyBase.class.getName());
+ }
+
+ @Test
+ void constructPrivateConstructor() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
+ assertThatThrownBy(() -> factory.construct(PrivateConstructor.class)).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void constructConflictingConstructors() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
+ assertThatThrownBy(() -> factory.construct(ConstructorConflict.class))
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void constructConflictingStatics() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
+ assertThatThrownBy(() -> factory.construct(StaticConflict.class))
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void constructConflictingMixed() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
+ assertThatThrownBy(() -> factory.construct(MixedConflict.class))
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void constructMissingTypedValue() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .namedArg("context", String.class)
+ .build();
+ assertThatThrownBy(() ->
+ factory.construct(SingleConstructor.class)
+ .namedArg("user", "Siphalor")
+ .namedArg("context", "world")
+ .finish()
+ ).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Integer");
+ }
+
+ @Test
+ void constructMissingNamedValue() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .namedArg("context", String.class)
+ .build();
+ assertThatThrownBy(() ->
+ factory.construct(SingleConstructor.class)
+ .typedArg(123)
+ .namedArg("context", "world")
+ .finish()
+ ).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("user");
+ }
+
+ @Test
+ void constructForSingleConstructor() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .namedArg("context", String.class)
+ .namedArg("other", String.class)
+ .build();
+ val result = factory.construct(SingleConstructor.class)
+ .typedArg(123)
+ .namedArg("user", "Siphalor")
+ .namedArg("context", "world")
+ .namedArg("other", "something")
+ .finish();
+ assertThat(result).isEqualTo(new SingleConstructor(123, "Siphalor", "world"));
+ }
+
+ @Test
+ void constructForStatic() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .namedArg("context", String.class)
+ .namedArg("other", String.class)
+ .build();
+ val result = factory.construct(Static.class)
+ .typedArg(123)
+ .namedArg("user", "Siphalor")
+ .namedArg("context", "world")
+ .namedArg("other", "something")
+ .finish();
+ assertThat(result).isEqualTo(new Static(1230, "Siphalor", "static"));
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ {
+ "de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyBase, base, 4560",
+ "de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyOtherBase, other, -456",
+ "de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyAltBase, alt, -4560",
+ }
+ )
+ void constructFindBase(Class super FindBase> base, String origin, int value) {
+ val factory = TweedConstructFactoryImpl.builder(base).typedArg(int.class).build();
+ val result = factory.construct(FindBase.class).typedArg(456).finish();
+ assertThat(result.origin()).isEqualTo(origin);
+ assertThat(result.value()).isEqualTo(value);
+ }
+
+ @Test
+ void constructPrimitives() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .typedArg(long.class)
+ .build();
+ val result = factory.construct(Primitives.class)
+ .typedArg(1)
+ .typedArg(2L)
+ .finish();
+ assertThat(result).isEqualTo(new Primitives(1, 2L));
+ }
+
+ @Test
+ void constructNamedCasting() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class).namedArg("test", Integer.class).build();
+ val result = factory.construct(NamedCasting.class).namedArg("test", 1234).finish();
+ assertThat(result.value()).isEqualTo(1234);
+ }
+
+ @Test
+ void constructDuplicateParams() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(String.class)
+ .typedArg(Long.class)
+ .namedArg("number", int.class)
+ .build();
+ assertThatThrownBy(() -> factory.construct(DuplicateParams.class))
+ .isInstanceOf(IllegalStateException.class)
+ .message()
+ .contains("java.lang.String", "number")
+ .containsIgnoringCase("typed")
+ .containsIgnoringCase("named")
+ .hasLineCount(3);
+ }
+
+ @Test
+ void constructUnexpectedTypedParameter() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Long.class)
+ .namedArg("user", String.class)
+ .build();
+ assertThatThrownBy(() -> factory.construct(Static.class))
+ .isInstanceOf(IllegalStateException.class)
+ .message()
+ .contains("java.lang.Integer")
+ .containsIgnoringCase("typed")
+ .hasLineCount(2);
+ }
+
+ @Test
+ void constructUnexpectedNamedParameter() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("other", String.class)
+ .build();
+ assertThatThrownBy(() -> factory.construct(Static.class))
+ .isInstanceOf(IllegalStateException.class)
+ .message()
+ .contains("user", "java.lang.String")
+ .containsIgnoringCase("named")
+ .hasLineCount(2);
+ }
+
+ @Test
+ void constructIllegalNamedType() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", Long.class)
+ .build();
+ assertThatThrownBy(() -> factory.construct(Static.class))
+ .isInstanceOf(IllegalStateException.class)
+ .message()
+ .contains("user", "java.lang.String", "java.lang.Long")
+ .containsIgnoringCase("named")
+ .hasLineCount(2);
+ }
+
+ @Test
+ void constructFinishUnknownTypedArgument() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .build();
+ assertThatThrownBy(() ->
+ factory.construct(Static.class)
+ .typedArg(12)
+ .namedArg("user", "Someone")
+ .typedArg(567L)
+ .finish()
+ ).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Long");
+ }
+
+ @Test
+ void constructFinishUnknownNamedArgument() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .build();
+ assertThatThrownBy(() ->
+ factory.construct(Static.class)
+ .typedArg(12)
+ .namedArg("user", "Someone")
+ .namedArg("other", "test")
+ .finish()
+ ).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("other");
+ }
+
+ @Test
+ void constructFinishNamedArgumentWrongType() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .build();
+ assertThatThrownBy(() ->
+ factory.construct(Static.class)
+ .typedArg(12)
+ .namedArg("user", 456L)
+ .finish())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("user", "java.lang.String", "java.lang.Long");
+ }
+
+ @Test
+ void constructFinishInconsistentNamedArgument() {
+ val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
+ .typedArg(Integer.class)
+ .namedArg("user", String.class)
+ .build();
+ //noinspection unchecked
+ assertThatThrownBy(() ->
+ factory.construct(Static.class)
+ .typedArg((Class)(Class>) String.class, 123)
+ .finish()
+ ).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ {
+ "boolean, java.lang.Boolean",
+ "java.lang.Boolean, java.lang.Boolean",
+ "byte, java.lang.Byte",
+ "java.lang.Byte, java.lang.Byte",
+ "char, java.lang.Character",
+ "java.lang.Character, java.lang.Character",
+ "short, java.lang.Short",
+ "java.lang.Short, java.lang.Short",
+ "int, java.lang.Integer",
+ "java.lang.Integer, java.lang.Integer",
+ "long, java.lang.Long",
+ "java.lang.Long, java.lang.Long",
+ "float, java.lang.Float",
+ "java.lang.Float, java.lang.Float",
+ "double, java.lang.Double",
+ "java.lang.Double, java.lang.Double",
+ "void, java.lang.Void",
+ "java.lang.Void, java.lang.Void",
+ "java.lang.String, java.lang.String",
+ }
+ )
+ void boxClass(Class> type, Class> expected) {
+ assertThat(TweedConstructFactoryImpl.boxClass(type)).isEqualTo(expected);
+ }
+
+ interface DummyBase {
+ }
+
+ interface DummyOtherBase {
+ }
+
+ interface DummyAltBase {
+ }
+
+ public static class MissingInheritance {
+ @TweedConstruct(DummyBase.class)
+ public MissingInheritance() {
+ }
+ }
+
+ @NoArgsConstructor(access = AccessLevel.PRIVATE)
+ public static class PrivateConstructor implements DummyBase {
+ }
+
+ public static class ConstructorConflict implements DummyBase {
+ @TweedConstruct(DummyBase.class)
+ public ConstructorConflict() {
+ }
+
+ @TweedConstruct(DummyBase.class)
+ public ConstructorConflict(String context) {
+ }
+ }
+
+ public static class StaticConflict implements DummyBase {
+ @TweedConstruct(DummyBase.class)
+ public static StaticConflict ofA() {
+ return null;
+ }
+
+ @TweedConstruct(DummyBase.class)
+ public static StaticConflict ofB() {
+ return null;
+ }
+ }
+
+ public static class MixedConflict implements DummyBase {
+ @TweedConstruct(DummyBase.class)
+ public MixedConflict() {
+ }
+
+ @TweedConstruct(DummyBase.class)
+ public static MixedConflict of() {
+ return null;
+ }
+ }
+
+ @Getter
+ @EqualsAndHashCode
+ public static class SingleConstructor implements DummyBase {
+ private final Integer times;
+ private final String user;
+ private final String context;
+
+ public SingleConstructor(
+ Integer times,
+ @ConstructParameter(name = "user") String user,
+ @ConstructParameter(name = "context") String context
+ ) {
+ this.times = times;
+ this.user = user;
+ this.context = context;
+ }
+ }
+
+ @Value
+ public static class Static implements DummyBase {
+ Integer times;
+ String user;
+ String context;
+
+ @TweedConstruct(DummyBase.class)
+ public static Static of(Integer times, @ConstructParameter(name = "user") String user) {
+ return new Static(times * 10, user, "static");
+ }
+ }
+
+ @Value
+ @AllArgsConstructor
+ public static class FindBase implements DummyBase, DummyOtherBase, DummyAltBase {
+ String origin;
+ int value;
+
+ @TweedConstruct(DummyBase.class)
+ public FindBase(int value) {
+ this("base", value * 10);
+ }
+
+ @TweedConstruct(DummyOtherBase.class)
+ public static FindBase ofOther(int value) {
+ return new FindBase("other", value * -1);
+ }
+
+ @TweedConstruct(DummyAltBase.class)
+ public static FindBase ofAlt(int value) {
+ return new FindBase("alt", value * -10);
+ }
+ }
+
+ @Value
+ public static class Primitives implements DummyBase {
+ int a;
+ Long b;
+ }
+
+ @Value
+ public static class NamedCasting implements DummyBase {
+ Number value;
+ public NamedCasting(@ConstructParameter(name = "test") Number value) {
+ this.value = value;
+ }
+ }
+
+ @Value
+ public static class DuplicateParams implements DummyBase {
+ public DuplicateParams(
+ String one,
+ String two,
+ Long test,
+ @ConstructParameter(name = "number") int three,
+ @ConstructParameter(name = "number") int four
+ ) {
+ }
+ }
+}
diff --git a/tweed5-core/build.gradle.kts b/tweed5-core/build.gradle.kts
index 1be49dd..e336e05 100644
--- a/tweed5-core/build.gradle.kts
+++ b/tweed5-core/build.gradle.kts
@@ -3,6 +3,7 @@ plugins {
}
dependencies {
+ implementation(project(":tweed5-construct"))
api(project(":tweed5-patchwork"))
api(project(":tweed5-utils"))
-}
\ No newline at end of file
+}
diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java
index 7de32ee..dd77dcd 100644
--- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java
+++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/container/ConfigContainer.java
@@ -1,5 +1,6 @@
package de.siphalor.tweed5.core.api.container;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
@@ -16,6 +17,8 @@ import java.util.Map;
* @see ConfigContainerSetupPhase
*/
public interface ConfigContainer {
+ @SuppressWarnings("rawtypes")
+ TweedConstructFactory FACTORY = TweedConstructFactory.builder(ConfigContainer.class).build();
default void registerExtensions(TweedExtension... extensions) {
for (TweedExtension extension : extensions) {
diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java
deleted file mode 100644
index 68a4b24..0000000
--- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/impl/entry/ReflectiveCompoundConfigEntryImpl.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package de.siphalor.tweed5.core.impl.entry;
-
-import de.siphalor.tweed5.core.api.entry.*;
-import lombok.Getter;
-import lombok.Value;
-import org.jetbrains.annotations.NotNull;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-@Getter
-public class ReflectiveCompoundConfigEntryImpl extends BaseConfigEntry implements CompoundConfigEntry {
- private final Constructor noArgsConstructor;
- private final Map compoundEntries;
-
- public ReflectiveCompoundConfigEntryImpl(Class valueClass) {
- super(valueClass);
- try {
- this.noArgsConstructor = valueClass.getConstructor();
- } catch (NoSuchMethodException e) {
- throw new IllegalArgumentException("Value class must have a no-arg constructor", e);
- }
- this.compoundEntries = new LinkedHashMap<>();
- }
-
- public void addSubEntry(String name, Field field, ConfigEntry> configEntry) {
- requireUnsealed();
-
- if (field.getType() != valueClass()) {
- throw new IllegalArgumentException("Field is not defined on the correct type");
- }
-
- //noinspection unchecked
- compoundEntries.put(name, new CompoundEntry(name, field, (ConfigEntry) configEntry));
- }
-
- public Map> subEntries() {
- return compoundEntries.values().stream().collect(Collectors.toMap(CompoundEntry::name, CompoundEntry::configEntry));
- }
-
- @Override
- public void set(T compoundValue, String key, V value) {
- CompoundEntry compoundEntry = compoundEntries.get(key);
- if (compoundEntry == null) {
- throw new IllegalArgumentException("Unknown config entry: " + key);
- }
-
- try {
- compoundEntry.field().set(compoundValue, value);
-
- } catch (IllegalAccessException e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Override
- public V get(T compoundValue, String key) {
- CompoundEntry compoundEntry = compoundEntries.get(key);
- if (compoundEntry == null) {
- throw new IllegalArgumentException("Unknown config entry: " + key);
- }
-
- try {
- //noinspection unchecked
- return (V) compoundEntry.field().get(compoundValue);
- } catch (IllegalAccessException e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Override
- public T instantiateCompoundValue() {
- try {
- return noArgsConstructor.newInstance();
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
- throw new IllegalStateException("Failed to instantiate compound value", e);
- }
- }
-
- @Override
- public void visitInOrder(ConfigEntryVisitor visitor) {
- if (visitor.enterCompoundEntry(this)) {
- for (Map.Entry entry : compoundEntries.entrySet()) {
- if (visitor.enterCompoundSubEntry(entry.getKey())) {
- entry.getValue().configEntry().visitInOrder(visitor);
- visitor.leaveCompoundSubEntry(entry.getKey());
- }
- }
- visitor.leaveCompoundEntry(this);
- }
- }
-
- @Override
- public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
- if (visitor.enterCompoundEntry(this, value)) {
- compoundEntries.forEach((key, entry) -> {
- if (visitor.enterCompoundSubEntry(key)) {
- try {
- visitor.visitEntry(entry.configEntry(), entry.field().get(value));
- } catch (IllegalAccessException ignored) {
- // ignored
- }
- visitor.leaveCompoundSubEntry(key);
- }
- });
- visitor.leaveCompoundEntry(this, value);
- }
- }
-
- @Override
- public @NotNull T deepCopy(@NotNull T value) {
- try {
- T copy = instantiateCompoundValue();
- for (CompoundEntry compoundEntry : compoundEntries.values()) {
- compoundEntry.field.set(copy, compoundEntry.field.get(value));
- }
- return copy;
- } catch (IllegalAccessException e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Value
- public static class CompoundEntry {
- String name;
- Field field;
- ConfigEntry configEntry;
- }
-}
diff --git a/tweed5-weaver-pojo/build.gradle.kts b/tweed5-weaver-pojo/build.gradle.kts
index e300c1d..80abcd9 100644
--- a/tweed5-weaver-pojo/build.gradle.kts
+++ b/tweed5-weaver-pojo/build.gradle.kts
@@ -3,7 +3,8 @@ plugins {
}
dependencies {
+ implementation(project(":tweed5-construct"))
api(project(":tweed5-core"))
api(project(":tweed5-naming-format"))
api(project(":tweed5-type-utils"))
-}
\ No newline at end of file
+}
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCollectionConfigEntry.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCollectionConfigEntry.java
index ec8e496..41f12cf 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCollectionConfigEntry.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCollectionConfigEntry.java
@@ -1,11 +1,9 @@
package de.siphalor.tweed5.weaver.pojo.api.entry;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
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;
@@ -15,26 +13,13 @@ import java.util.function.IntFunction;
* 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 WeavableCollectionConfigEntry>
- extends CollectionConfigEntry {
- static , C extends WeavableCollectionConfigEntry> C instantiate(
- Class weavableClass, Class valueClass, IntFunction constructor
- ) throws PojoWeavingException {
- try {
- Constructor 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
- );
- }
- }
+public interface WeavableCollectionConfigEntry> extends CollectionConfigEntry {
+ @SuppressWarnings("rawtypes")
+ TweedConstructFactory FACTORY =
+ TweedConstructFactory.builder(WeavableCollectionConfigEntry.class)
+ .typedArg(Class.class) // value class
+ .typedArg(IntFunction.class) // value class constructor with capacity
+ .build();
void elementEntry(ConfigEntry elementEntry);
}
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java
index 0244d0a..aefb089 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/entry/WeavableCompoundConfigEntry.java
@@ -1,40 +1,27 @@
package de.siphalor.tweed5.weaver.pojo.api.entry;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
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;
+import java.util.function.Supplier;
/**
* {@inheritDoc}
*
- * A constructor taking the value {@link Class} and a {@link MethodHandle} that allows to instantiate the value Class with no arguments.
+ * A constructor taking the value {@link Class} and a {@link Supplier} that allows to instantiate the value Class with no arguments.
*/
public interface WeavableCompoundConfigEntry extends CompoundConfigEntry {
- static > C instantiate(
- Class weavableClass, Class valueClass, MethodHandle constructorHandle
- ) throws PojoWeavingException {
- try {
- Constructor 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
- );
- }
- }
+ @SuppressWarnings("rawtypes")
+ TweedConstructFactory FACTORY =
+ TweedConstructFactory.builder(WeavableCompoundConfigEntry.class)
+ .typedArg(Class.class) // the value class
+ .typedArg(Supplier.class) // constructor for the value class
+ .build();
void registerSubEntry(SubEntry subEntry);
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CollectionPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CollectionPojoWeaver.java
index 6ece4ff..8f9972d 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CollectionPojoWeaver.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CollectionPojoWeaver.java
@@ -31,6 +31,7 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
this.weavingConfigAccess = context.registerWeavingContextExtensionData(CollectionWeavingConfig.class);
}
+ @SuppressWarnings({"rawtypes", "unchecked"})
@Override
public @Nullable ConfigEntry weaveEntry(ActualType valueType, WeavingContext context) {
List> collectionTypeParams = valueType.getTypesOfSuperArguments(Collection.class);
@@ -44,12 +45,11 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
IntFunction> constructor = getCollectionConstructor(valueType);
- //noinspection unchecked,rawtypes
- WeavableCollectionConfigEntry configEntry = WeavableCollectionConfigEntry.instantiate(
- (Class) weavingConfig.collectionEntryClass(),
- (Class) valueType.declaredType(),
- constructor
- );
+ WeavableCollectionConfigEntry configEntry = WeavableCollectionConfigEntry.FACTORY
+ .construct(Objects.requireNonNull(weavingConfig.collectionEntryClass()))
+ .typedArg(valueType.declaredType())
+ .typedArg(IntFunction.class, constructor)
+ .finish();
configEntry.elementEntry(context.weaveEntry(
collectionTypeParams.get(0),
@@ -62,7 +62,7 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
return configEntry;
} catch (Exception e) {
- throw new PojoWeavingException("Exception occurred trying to weave collectoin for class " + valueType, e);
+ throw new PojoWeavingException("Exception occurred trying to weave collection for class " + valueType, e);
}
}
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java
index acc4493..57adc1e 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaver.java
@@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandle;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.Map;
+import java.util.function.Supplier;
/**
* A weaver that weaves classes with the {@link CompoundWeaving} annotation as compound entries.
@@ -112,10 +113,17 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
PojoClassIntrospector classIntrospector,
CompoundWeavingConfig weavingConfig
) {
- MethodHandle valueConstructor = classIntrospector.noArgsConstructor();
- if (valueConstructor == null) {
+ MethodHandle valueConstructorHandle = classIntrospector.noArgsConstructor();
+ if (valueConstructorHandle == null) {
throw new PojoWeavingException("Class " + classIntrospector.type().getName() + " must have public no args constructor");
}
+ Supplier> valueConstructor = () -> {
+ try {
+ return valueConstructorHandle.invoke();
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ };
//noinspection rawtypes
Class extends WeavableCompoundConfigEntry> annotationEntryClass = weavingConfig.compoundEntryClass();
@@ -125,11 +133,10 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
? annotationEntryClass
: StaticPojoCompoundConfigEntry.class
);
- return WeavableCompoundConfigEntry.instantiate(
- weavableEntryClass,
- (Class) classIntrospector.type(),
- valueConstructor
- );
+ return WeavableCompoundConfigEntry.FACTORY.construct(weavableEntryClass)
+ .typedArg(classIntrospector.type())
+ .typedArg(Supplier.class, valueConstructor)
+ .finish();
}
private boolean shouldIncludeCompoundPropertyInWeaving(PojoClassIntrospector.Property property) {
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java
index ed1a375..0af0c10 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/TweedPojoWeaver.java
@@ -1,9 +1,11 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import org.jetbrains.annotations.ApiStatus;
public interface TweedPojoWeaver extends TweedPojoWeavingFunction {
+ TweedConstructFactory FACTORY = TweedConstructFactory.builder(TweedPojoWeaver.class).build();
@ApiStatus.OverrideOnly
void setup(SetupContext context);
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java
index be2823d..65e9f25 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/weaving/postprocess/TweedPojoWeavingPostProcessor.java
@@ -1,8 +1,12 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess;
+import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
public interface TweedPojoWeavingPostProcessor {
+ TweedConstructFactory FACTORY =
+ TweedConstructFactory.builder(TweedPojoWeavingPostProcessor.class).build();
+
void apply(ConfigEntry> configEntry, WeavingContext context);
-}
\ No newline at end of file
+}
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java
index 5029fbc..7f146b2 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/entry/StaticPojoCompoundConfigEntry.java
@@ -7,17 +7,17 @@ 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.LinkedHashMap;
import java.util.Map;
+import java.util.function.Supplier;
public class StaticPojoCompoundConfigEntry extends BaseConfigEntry implements WeavableCompoundConfigEntry {
- private final MethodHandle noArgsConstructor;
+ private final Supplier noArgsConstructor;
private final Map subEntries = new LinkedHashMap<>();
private final Map> subConfigEntries = new LinkedHashMap<>();
- public StaticPojoCompoundConfigEntry(@NotNull Class valueClass, @NotNull MethodHandle noArgsConstructor) {
+ public StaticPojoCompoundConfigEntry(@NotNull Class valueClass, @NotNull Supplier noArgsConstructor) {
super(valueClass);
this.noArgsConstructor = noArgsConstructor;
}
@@ -66,8 +66,7 @@ public class StaticPojoCompoundConfigEntry extends BaseConfigEntry impleme
@Override
public T instantiateCompoundValue() {
try {
- //noinspection unchecked
- return (T) noArgsConstructor.invoke();
+ return noArgsConstructor.get();
} catch (Throwable e) {
throw new IllegalStateException("Failed to instantiate compound class", e);
}
diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java
index 6cecdb9..7df96c6 100644
--- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java
+++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapper.java
@@ -19,8 +19,6 @@ 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.*;
import java.util.stream.Collectors;
@@ -63,19 +61,19 @@ public class TweedPojoWeaverBootstrapper {
private static Collection loadWeavers(Collection> weaverClasses) {
return weaverClasses.stream()
- .map(weaverClass -> checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass))
+ .map(weaverClass -> TweedPojoWeaver.FACTORY.construct(weaverClass).finish())
.collect(Collectors.toList());
}
private static Collection loadPostProcessors(Collection> postProcessorClasses) {
return postProcessorClasses.stream()
- .map(postProcessorClass -> checkImplementsAndInstantiate(TweedPojoWeavingPostProcessor.class, postProcessorClass))
+ .map(postProcessorClass -> TweedPojoWeavingPostProcessor.FACTORY.construct(postProcessorClass).finish())
.collect(Collectors.toList());
}
private static ConfigContainer> createConfigContainer(Class extends ConfigContainer>> containerClass) {
try {
- return checkImplementsAndInstantiate(ConfigContainer.class, containerClass);
+ return ConfigContainer.FACTORY.construct(containerClass).finish();
} catch (Exception e) {
throw new PojoWeavingException("Failed to instantiate config container");
}
@@ -144,23 +142,6 @@ public class TweedPojoWeaverBootstrapper {
}
}
- private static T checkImplementsAndInstantiate(Class 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 instantiate(Class clazz) {
- try {
- Constructor constructor = clazz.getConstructor();
- return constructor.newInstance();
- } catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
- IllegalAccessException e) {
- throw new PojoWeavingException("Failed to instantiate class " + clazz.getName(), e);
- }
- }
-
public ConfigContainer weave() {
setupWeavers();
WeavingContext weavingContext = createWeavingContext();
diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java
index 6a463ea..7a25c55 100644
--- a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java
+++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/api/weaving/CompoundPojoWeaverTest.java
@@ -121,4 +121,4 @@ class CompoundPojoWeaverTest {
return weavingConfig.compoundEntryClass();
}
}
-}
\ No newline at end of file
+}