diff --git a/settings.gradle.kts b/settings.gradle.kts index 09617f3..d3199f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ rootProject.name = "tweed5" +include("tweed5-construct") include("tweed5-core") include("tweed5-default-extensions") include("tweed5-naming-format") diff --git a/tweed5-construct/build.gradle.kts b/tweed5-construct/build.gradle.kts new file mode 100644 index 0000000..4943a3b --- /dev/null +++ b/tweed5-construct/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("de.siphalor.tweed5.base-module") +} diff --git a/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/ConstructParameter.java b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/ConstructParameter.java new file mode 100644 index 0000000..b16bde2 --- /dev/null +++ b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/ConstructParameter.java @@ -0,0 +1,19 @@ +package de.siphalor.tweed5.construct.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Provides more information about a parameter of a {@link TweedConstruct}. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ConstructParameter { + /** + * Allows defining a named parameter with the given name. + * Parameters with this set are not considered as typed parameters. + */ + String name(); +} diff --git a/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/TweedConstruct.java b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/TweedConstruct.java new file mode 100644 index 0000000..daf1655 --- /dev/null +++ b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/TweedConstruct.java @@ -0,0 +1,21 @@ +package de.siphalor.tweed5.construct.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a method or constructor that should be used for construction using {@link TweedConstructFactory}. + *

+ * There must only be a single annotation for a certain target class on any constructor or static method of a class. + */ +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TweedConstruct { + /** + * Defines the target base class that this constructor may be used to create. + * This is the base class defined in {@link TweedConstructFactory#builder(Class)}. + */ + Class value(); +} diff --git a/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/TweedConstructFactory.java b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/TweedConstructFactory.java new file mode 100644 index 0000000..d15e077 --- /dev/null +++ b/tweed5-construct/src/main/java/de/siphalor/tweed5/construct/api/TweedConstructFactory.java @@ -0,0 +1,99 @@ +package de.siphalor.tweed5.construct.api; + +import de.siphalor.tweed5.construct.impl.TweedConstructFactoryImpl; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A factory that allows to construct instances of subclasses of a specific type. + *

+ * This factory basically extends the interfaces' contract to include + * a public constructor or public static method with any of the defined arguments. + *

+ * The factory should usually be defined as a public static final member of the base class. + * + * @param the base class/interface + */ +public interface TweedConstructFactory { + /** + * Starts building a new factory for the given base class. + */ + static TweedConstructFactory.@NotNull FactoryBuilder builder(Class baseClass) { + return TweedConstructFactoryImpl.builder(baseClass); + } + + /** + * Starts the instantiation process for a subclass. + * All defined arguments must be bound to values. + */ + @CheckReturnValue + @Contract(pure = true) + @NotNull Construct construct(@NotNull Class subClass); + + /** + * Builder for the factory. + */ + interface FactoryBuilder { + /** + * Defines a new typed argument of the given type. + */ + @Contract(mutates = "this", value = "_ -> this") + @NotNull FactoryBuilder typedArg(@NotNull Class argType); + + /** + * Defines a new named argument with the given name and value type. + */ + @Contract(mutates = "this", value = "_, _ -> this") + @NotNull FactoryBuilder namedArg(@NotNull String name, @NotNull Class argType); + + /** + * Builds the factory. + */ + @Contract(pure = true) + @NotNull TweedConstructFactory build(); + } + + /** + * Builder-style helper for the instantiation process. + *

+ * Allows to successively bind all previously defined arguments to actual values. + *

+ * Any method call in this class may perform checks against the defined arguments and throw according exceptions. + */ + interface Construct { + /** + * Binds a value to a typed argument of the exact same class. + *

+ * This will not work if the given value merely inherits from the defined class. + * Use {@link #typedArg(Class, Object)} for these cases instead. + * @see #namedArg(String, Object) + */ + @Contract(mutates = "this", value = "_ -> this") + @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 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 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) (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 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 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> 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 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 +}