[construct, core, weaver-pojo] Replace simple class injections with more sophisticated construction
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
rootProject.name = "tweed5"
|
||||
|
||||
include("tweed5-construct")
|
||||
include("tweed5-core")
|
||||
include("tweed5-default-extensions")
|
||||
include("tweed5-naming-format")
|
||||
|
||||
3
tweed5-construct/build.gradle.kts
Normal file
3
tweed5-construct/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id("de.siphalor.tweed5.base-module")
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* This factory basically extends the interfaces' contract to include
|
||||
* a public constructor or public static method with any of the defined arguments.
|
||||
* <p>
|
||||
* The factory should usually be defined as a public static final member of the base class.
|
||||
*
|
||||
* @param <T> the base class/interface
|
||||
*/
|
||||
public interface TweedConstructFactory<T> {
|
||||
/**
|
||||
* Starts building a new factory for the given base class.
|
||||
*/
|
||||
static <T> TweedConstructFactory.@NotNull FactoryBuilder<T> builder(Class<T> baseClass) {
|
||||
return TweedConstructFactoryImpl.builder(baseClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the instantiation process for a subclass.
|
||||
* All defined arguments must be bound to values.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
@Contract(pure = true)
|
||||
<C extends T> @NotNull Construct<C> construct(@NotNull Class<C> subClass);
|
||||
|
||||
/**
|
||||
* Builder for the factory.
|
||||
*/
|
||||
interface FactoryBuilder<T> {
|
||||
/**
|
||||
* Defines a new typed argument of the given type.
|
||||
*/
|
||||
@Contract(mutates = "this", value = "_ -> this")
|
||||
<A> @NotNull FactoryBuilder<T> typedArg(@NotNull Class<A> argType);
|
||||
|
||||
/**
|
||||
* Defines a new named argument with the given name and value type.
|
||||
*/
|
||||
@Contract(mutates = "this", value = "_, _ -> this")
|
||||
<A> @NotNull FactoryBuilder<T> namedArg(@NotNull String name, @NotNull Class<A> argType);
|
||||
|
||||
/**
|
||||
* Builds the factory.
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
@NotNull TweedConstructFactory<T> build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder-style helper for the instantiation process.
|
||||
* <p>
|
||||
* Allows to successively bind all previously defined arguments to actual values.
|
||||
* <p>
|
||||
* Any method call in this class may perform checks against the defined arguments and throw according exceptions.
|
||||
*/
|
||||
interface Construct<C> {
|
||||
/**
|
||||
* Binds a value to a typed argument of the exact same class.
|
||||
* <p>
|
||||
* 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")
|
||||
<A> @NotNull Construct<C> typedArg(@NotNull A value);
|
||||
|
||||
/**
|
||||
* Binds a value to a typed argument of the given type.
|
||||
* <p>
|
||||
* This allows binding the value to super classes of the value.
|
||||
* @see #typedArg(Object)
|
||||
* @see #namedArg(String, Object)
|
||||
*/
|
||||
@Contract(mutates = "this", value = "_, _ -> this")
|
||||
<A> @NotNull Construct<C> typedArg(@NotNull Class<? super A> argType, @Nullable A value);
|
||||
|
||||
/**
|
||||
* Binds a value to a named argument.
|
||||
* @see #typedArg(Object)
|
||||
*/
|
||||
@Contract(mutates = "this", value = "_, _ -> this")
|
||||
<A> @NotNull Construct<C> namedArg(@NotNull String name, @Nullable A value);
|
||||
|
||||
/**
|
||||
* Finishes the binding and actually constructs the class.
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
@NotNull C finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.siphalor.tweed5.construct.impl;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
class Entry<K, V> {
|
||||
K key;
|
||||
V value;
|
||||
}
|
||||
@@ -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<T> implements TweedConstructFactory<T> {
|
||||
private static final int CONSTRUCTOR_MODIFIERS = Modifier.PUBLIC;
|
||||
private static final int STATIC_METHOD_MODIFIERS = Modifier.PUBLIC | Modifier.STATIC;
|
||||
|
||||
private final Class<T> constructBaseClass;
|
||||
private final Set<Class<?>> typedArgs;
|
||||
private final Map<String, Class<?>> namedArgs;
|
||||
private final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
private final Map<Class<?>, Optional<ConstructTarget<?>>> cachedConstructTargets = new HashMap<>();
|
||||
@SuppressWarnings("unused")
|
||||
private final ReadWriteLock cachedConstructTargetsLock = new ReentrantReadWriteLock();
|
||||
|
||||
public static <T> TweedConstructFactoryImpl.FactoryBuilder<T> builder(Class<T> baseClass) {
|
||||
return new FactoryBuilder<>(baseClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <C extends T> TweedConstructFactory.@NotNull Construct<C> construct(@NotNull Class<C> subClass) {
|
||||
return new Construct<>(getConstructTarget(subClass));
|
||||
}
|
||||
|
||||
private <C extends T> @NotNull ConstructTarget<C> getConstructTarget(Class<C> type) {
|
||||
ConstructTarget<C> cachedConstructTarget = readConstructTargetFromCache(type);
|
||||
if (cachedConstructTarget != null) {
|
||||
return cachedConstructTarget;
|
||||
}
|
||||
ConstructTarget<C> constructTarget = locateConstructTarget(type);
|
||||
cacheConstructTarget(type, constructTarget);
|
||||
return constructTarget;
|
||||
}
|
||||
|
||||
@Locked.Read("cachedConstructTargetsLock")
|
||||
private <C extends T> @Nullable ConstructTarget<C> readConstructTargetFromCache(Class<C> type) {
|
||||
Optional<ConstructTarget<?>> 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<C>) cachedConstructTarget.get();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Locked.Write("cachedConstructTargetsLock")
|
||||
private <C extends T> void cacheConstructTarget(Class<C> type, ConstructTarget<C> constructTarget) {
|
||||
cachedConstructTargets.put(type, Optional.of(constructTarget));
|
||||
}
|
||||
|
||||
private <C extends T> ConstructTarget<C> locateConstructTarget(Class<C> type) {
|
||||
if (!constructBaseClass.isAssignableFrom(type)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + type.getName() + " is not a subclass of " + constructBaseClass.getName()
|
||||
);
|
||||
}
|
||||
|
||||
Collection<Constructor<?>> constructorCandidates = findConstructorCandidates(type);
|
||||
Collection<Method> staticConstructorCandidates = findStaticConstructorCandidates(type);
|
||||
|
||||
List<Executable> 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<Constructor<?>> findConstructorCandidates(Class<?> type) {
|
||||
return Arrays.stream(type.getConstructors())
|
||||
.filter(constructor -> (constructor.getModifiers() & CONSTRUCTOR_MODIFIERS) == CONSTRUCTOR_MODIFIERS)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Collection<Method> 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 <C extends T> ConstructTarget<C> resolveConstructTarget(Class<C> type, Executable executable) {
|
||||
Object[] argOrder = new Object[executable.getParameterCount()];
|
||||
|
||||
Map<Class<?>, List<Parameter>> typedParameters = new HashMap<>();
|
||||
Map<String, List<Parameter>> 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<Parameter> 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<Parameter> 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 <C> Function<Object[], C> createInvokerFromCandidate(Class<C> 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<Class<?>, List<Parameter>> typedParameters,
|
||||
Map<String, List<Parameter>> 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<Class<?>> 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<String> 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<T> implements TweedConstructFactory.FactoryBuilder<T> {
|
||||
private final Class<T> constructBaseClass;
|
||||
private final Set<Class<?>> typedArgs = new HashSet<>();
|
||||
private final Map<String, Class<?>> namedArgs = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public <A> TweedConstructFactory.@NotNull FactoryBuilder<T> typedArg(@NotNull Class<A> 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 <A> TweedConstructFactory.@NotNull FactoryBuilder<T> namedArg(
|
||||
@NotNull String name,
|
||||
@NotNull Class<A> 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<T> build() {
|
||||
return new TweedConstructFactoryImpl<>(
|
||||
constructBaseClass,
|
||||
typedArgs,
|
||||
namedArgs
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private class Construct<C> implements TweedConstructFactory.Construct<C> {
|
||||
private final ConstructTarget<C> target;
|
||||
private final Map<Class<?>, Object> typedArgValues = new HashMap<>();
|
||||
private final Map<String, Object> namedArgValues = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public <A> TweedConstructFactory.@NotNull Construct<C> typedArg(@NotNull A value) {
|
||||
requireTypedArgExists(value.getClass(), value);
|
||||
typedArgValues.put(value.getClass(), value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A> TweedConstructFactory.@NotNull Construct<C> 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 <A> 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 <A> TweedConstructFactory.@NotNull Construct<C> 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<Class<?>> missingTypedArgs = Collections.emptySet();
|
||||
if (typedArgValues.size() != typedArgs.size()) {
|
||||
missingTypedArgs = new HashSet<>(typedArgs);
|
||||
missingTypedArgs.removeAll(typedArgValues.keySet());
|
||||
}
|
||||
Set<String> 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<Class<?>> missingTypedArgs, Set<String> 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 <V> Class<V> boxClass(Class<V> type) {
|
||||
if (!type.isPrimitive()) {
|
||||
return type;
|
||||
}
|
||||
if (type == boolean.class) {
|
||||
return (Class<V>) Boolean.class;
|
||||
} else if (type == byte.class) {
|
||||
return (Class<V>) Byte.class;
|
||||
} else if (type == char.class) {
|
||||
return (Class<V>) Character.class;
|
||||
} else if (type == short.class) {
|
||||
return (Class<V>) Short.class;
|
||||
} else if (type == int.class) {
|
||||
return (Class<V>) Integer.class;
|
||||
} else if (type == long.class) {
|
||||
return (Class<V>) Long.class;
|
||||
} else if (type == float.class) {
|
||||
return (Class<V>) Float.class;
|
||||
} else if (type == double.class) {
|
||||
return (Class<V>) Double.class;
|
||||
} else if (type == void.class) {
|
||||
return (Class<V>) Void.class;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported primitive type " + type);
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
private static class ConstructTarget<C> {
|
||||
Class<?> type;
|
||||
Object[] argOrder;
|
||||
Function<Object[], C> invoker;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Internal
|
||||
package de.siphalor.tweed5.construct.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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<DummyBase> 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<DummyBase> 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<Object>)(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
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":tweed5-construct"))
|
||||
api(project(":tweed5-patchwork"))
|
||||
api(project(":tweed5-utils"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
@SuppressWarnings("rawtypes")
|
||||
TweedConstructFactory<ConfigContainer> FACTORY = TweedConstructFactory.builder(ConfigContainer.class).build();
|
||||
|
||||
default void registerExtensions(TweedExtension... extensions) {
|
||||
for (TweedExtension extension : extensions) {
|
||||
|
||||
@@ -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<T> extends BaseConfigEntry<T> implements CompoundConfigEntry<T> {
|
||||
private final Constructor<T> noArgsConstructor;
|
||||
private final Map<String, CompoundEntry> compoundEntries;
|
||||
|
||||
public ReflectiveCompoundConfigEntryImpl(Class<T> 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<Object>) configEntry));
|
||||
}
|
||||
|
||||
public Map<String, ConfigEntry<?>> subEntries() {
|
||||
return compoundEntries.values().stream().collect(Collectors.toMap(CompoundEntry::name, CompoundEntry::configEntry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> 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> 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<String, CompoundEntry> 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<Object> configEntry;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":tweed5-construct"))
|
||||
api(project(":tweed5-core"))
|
||||
api(project(":tweed5-naming-format"))
|
||||
api(project(":tweed5-type-utils"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<E, T extends Collection<E>>
|
||||
extends CollectionConfigEntry<E, T> {
|
||||
static <E, T extends Collection<E>, C extends WeavableCollectionConfigEntry<E, T>> C instantiate(
|
||||
Class<C> weavableClass, Class<T> valueClass, IntFunction<T> constructor
|
||||
) throws PojoWeavingException {
|
||||
try {
|
||||
Constructor<C> weavableEntryConstructor = weavableClass.getConstructor(Class.class, IntFunction.class);
|
||||
return weavableEntryConstructor.newInstance(valueClass, constructor);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new PojoWeavingException(
|
||||
"Class " + weavableClass.getName() + " must have constructor with value class and value constructor",
|
||||
e
|
||||
);
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new PojoWeavingException(
|
||||
"Failed to instantiate class for weavable collection entry " + weavableClass.getName(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
public interface WeavableCollectionConfigEntry<E, T extends Collection<E>> extends CollectionConfigEntry<E, T> {
|
||||
@SuppressWarnings("rawtypes")
|
||||
TweedConstructFactory<WeavableCollectionConfigEntry> FACTORY =
|
||||
TweedConstructFactory.builder(WeavableCollectionConfigEntry.class)
|
||||
.typedArg(Class.class) // value class
|
||||
.typedArg(IntFunction.class) // value class constructor with capacity
|
||||
.build();
|
||||
|
||||
void elementEntry(ConfigEntry<E> elementEntry);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
* <br />
|
||||
* 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<T> extends CompoundConfigEntry<T> {
|
||||
static <T, C extends WeavableCompoundConfigEntry<T>> C instantiate(
|
||||
Class<C> weavableClass, Class<T> valueClass, MethodHandle constructorHandle
|
||||
) throws PojoWeavingException {
|
||||
try {
|
||||
Constructor<C> weavableEntryConstructor = weavableClass.getConstructor(Class.class, MethodHandle.class);
|
||||
return weavableEntryConstructor.newInstance(valueClass, constructorHandle);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new PojoWeavingException(
|
||||
"Class " + weavableClass.getName() + " must have constructor with value class and value constructor",
|
||||
e
|
||||
);
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new PojoWeavingException(
|
||||
"Failed to instantiate class for weavable compound entry " + weavableClass.getName(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
@SuppressWarnings("rawtypes")
|
||||
TweedConstructFactory<WeavableCompoundConfigEntry> FACTORY =
|
||||
TweedConstructFactory.builder(WeavableCompoundConfigEntry.class)
|
||||
.typedArg(Class.class) // the value class
|
||||
.typedArg(Supplier.class) // constructor for the value class
|
||||
.build();
|
||||
|
||||
void registerSubEntry(SubEntry subEntry);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
|
||||
this.weavingConfigAccess = context.registerWeavingContextExtensionData(CollectionWeavingConfig.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Override
|
||||
public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
|
||||
List<ActualType<?>> collectionTypeParams = valueType.getTypesOfSuperArguments(Collection.class);
|
||||
@@ -44,12 +45,11 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
|
||||
|
||||
IntFunction<Collection<Object>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<C>) classIntrospector.type(),
|
||||
valueConstructor
|
||||
);
|
||||
return WeavableCompoundConfigEntry.FACTORY.construct(weavableEntryClass)
|
||||
.typedArg(classIntrospector.type())
|
||||
.typedArg(Supplier.class, valueConstructor)
|
||||
.finish();
|
||||
}
|
||||
|
||||
private boolean shouldIncludeCompoundPropertyInWeaving(PojoClassIntrospector.Property property) {
|
||||
|
||||
@@ -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<TweedPojoWeaver> FACTORY = TweedConstructFactory.builder(TweedPojoWeaver.class).build();
|
||||
|
||||
@ApiStatus.OverrideOnly
|
||||
void setup(SetupContext context);
|
||||
|
||||
@@ -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<TweedPojoWeavingPostProcessor> FACTORY =
|
||||
TweedConstructFactory.builder(TweedPojoWeavingPostProcessor.class).build();
|
||||
|
||||
void apply(ConfigEntry<?> configEntry, WeavingContext context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> extends BaseConfigEntry<T> implements WeavableCompoundConfigEntry<T> {
|
||||
private final MethodHandle noArgsConstructor;
|
||||
private final Supplier<T> noArgsConstructor;
|
||||
private final Map<String, SubEntry> subEntries = new LinkedHashMap<>();
|
||||
private final Map<String, ConfigEntry<?>> subConfigEntries = new LinkedHashMap<>();
|
||||
|
||||
public StaticPojoCompoundConfigEntry(@NotNull Class<T> valueClass, @NotNull MethodHandle noArgsConstructor) {
|
||||
public StaticPojoCompoundConfigEntry(@NotNull Class<T> valueClass, @NotNull Supplier<T> noArgsConstructor) {
|
||||
super(valueClass);
|
||||
this.noArgsConstructor = noArgsConstructor;
|
||||
}
|
||||
@@ -66,8 +66,7 @@ public class StaticPojoCompoundConfigEntry<T> extends BaseConfigEntry<T> 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);
|
||||
}
|
||||
|
||||
@@ -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<T> {
|
||||
|
||||
private static Collection<TweedPojoWeaver> loadWeavers(Collection<Class<? extends TweedPojoWeaver>> weaverClasses) {
|
||||
return weaverClasses.stream()
|
||||
.map(weaverClass -> checkImplementsAndInstantiate(TweedPojoWeaver.class, weaverClass))
|
||||
.map(weaverClass -> TweedPojoWeaver.FACTORY.construct(weaverClass).finish())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Collection<TweedPojoWeavingPostProcessor> loadPostProcessors(Collection<Class<? extends TweedPojoWeavingPostProcessor>> 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<T> {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T checkImplementsAndInstantiate(Class<T> superClass, Class<? extends T> clazz) {
|
||||
if (!superClass.isAssignableFrom(clazz)) {
|
||||
throw new PojoWeavingException("Class " + clazz.getName() + " must extend/implement " + superClass.getName());
|
||||
}
|
||||
return instantiate(clazz);
|
||||
}
|
||||
|
||||
private static <T> T instantiate(Class<T> clazz) {
|
||||
try {
|
||||
Constructor<T> constructor = clazz.getConstructor();
|
||||
return constructor.newInstance();
|
||||
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
|
||||
IllegalAccessException e) {
|
||||
throw new PojoWeavingException("Failed to instantiate class " + clazz.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public ConfigContainer<T> weave() {
|
||||
setupWeavers();
|
||||
WeavingContext weavingContext = createWeavingContext();
|
||||
|
||||
@@ -121,4 +121,4 @@ class CompoundPojoWeaverTest {
|
||||
return weavingConfig.compoundEntryClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user