| 1 | package pro.verron.officestamper.core; | |
| 2 | ||
| 3 | import org.jspecify.annotations.Nullable; | |
| 4 | import org.springframework.core.convert.TypeDescriptor; | |
| 5 | import org.springframework.expression.EvaluationContext; | |
| 6 | import org.springframework.expression.MethodExecutor; | |
| 7 | import org.springframework.expression.MethodResolver; | |
| 8 | import org.springframework.expression.TypedValue; | |
| 9 | import pro.verron.officestamper.api.CustomFunction; | |
| 10 | ||
| 11 | import java.util.ArrayDeque; | |
| 12 | import java.util.Arrays; | |
| 13 | import java.util.List; | |
| 14 | import java.util.Map; | |
| 15 | import java.util.Map.Entry; | |
| 16 | import java.util.function.Function; | |
| 17 | import java.util.stream.Stream; | |
| 18 | ||
| 19 | import static java.util.Arrays.stream; | |
| 20 | import static java.util.Collections.emptyMap; | |
| 21 | import static java.util.stream.Collectors.groupingBy; | |
| 22 | import static java.util.stream.Collectors.toMap; | |
| 23 | ||
| 24 | /// The Invokers class serves as an implementation of the MethodResolver interface. | |
| 25 | /// | |
| 26 | /// It is designed to provide an efficient mechanism for resolving method executors based on method names and argument | |
| 27 | /// types. | |
| 28 | /// | |
| 29 | /// The class organizes and stores registered invokers in a structured map, enabling streamlined method resolution at | |
| 30 | /// runtime. | |
| 31 | public class Invokers | |
| 32 | implements MethodResolver { | |
| 33 | private final Map<String, Map<Args, MethodExecutor>> map; | |
| 34 | ||
| 35 | /// Constructs an `Invokers` instance, grouping and mapping invokers by their names and argument types to their | |
| 36 | /// corresponding executors. | |
| 37 | /// | |
| 38 | /// @param invokerStream a stream of `Invoker` objects, where each invoker encapsulates the method name, its | |
| 39 | /// parameter types, and the associated method executor. | |
| 40 | public Invokers(Stream<Invoker> invokerStream) { | |
| 41 | map = invokerStream.collect(groupingBy(Invoker::name, toMap(Invoker::args, Invoker::executor))); | |
| 42 | } | |
| 43 | ||
| 44 | /// Transforms a map containing interface-to-implementation mappings into a stream of `Invoker` objects. Each entry | |
| 45 | /// in the map is processed to generate a flat stream of relevant `Invoker` instances. | |
| 46 | /// | |
| 47 | /// @param interfaces2implementations a map where keys represent interface classes and values represent | |
| 48 | /// their corresponding implementations, used to derive invoker instances. | |
| 49 | /// | |
| 50 | /// @return a stream of `Invoker` objects derived from the provided map entries. | |
| 51 | public static Stream<Invoker> streamInvokersFromClass(Map<Class<?>, ?> interfaces2implementations) { | |
| 52 |
1
1. streamInvokersFromClass : replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokersFromClass → TIMED_OUT |
return interfaces2implementations.entrySet() |
| 53 | .stream() | |
| 54 | .flatMap(Invokers::streamInvokers); | |
| 55 | } | |
| 56 | ||
| 57 | private static Stream<Invoker> streamInvokers(Entry<Class<?>, ?> interface2implementation) { | |
| 58 |
1
1. streamInvokers : replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → TIMED_OUT |
return streamInvokers(interface2implementation.getKey(), interface2implementation.getValue()); |
| 59 | } | |
| 60 | ||
| 61 | private static Stream<Invoker> streamInvokers(Class<?> key, Object obj) { | |
| 62 |
2
1. streamInvokers : replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → TIMED_OUT 2. lambda$streamInvokers$0 : replaced return value with null for pro/verron/officestamper/core/Invokers::lambda$streamInvokers$0 → KILLED |
return stream(key.getDeclaredMethods()).map(method -> new Invoker(obj, method)); |
| 63 | } | |
| 64 | ||
| 65 | static Stream<Invoker> streamInvokersFromCustomFunction(List<CustomFunction> functions) { | |
| 66 |
1
1. streamInvokersFromCustomFunction : replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokersFromCustomFunction → TIMED_OUT |
return functions.stream() |
| 67 | .map(Invokers::ofCustomFunction); | |
| 68 | } | |
| 69 | ||
| 70 | /// Creates an [Invoker] for a custom function. | |
| 71 | /// | |
| 72 | /// @param cf the custom function. | |
| 73 | /// | |
| 74 | /// @return the invoker. | |
| 75 | public static Invoker ofCustomFunction(CustomFunction cf) { | |
| 76 | var cfName = cf.name(); | |
| 77 | var cfArgs = new Args(cf.parameterTypes()); | |
| 78 | var cfExecutor = new CustomFunctionExecutor(cf.function()); | |
| 79 |
1
1. ofCustomFunction : replaced return value with null for pro/verron/officestamper/core/Invokers::ofCustomFunction → KILLED |
return new Invoker(cfName, cfArgs, cfExecutor); |
| 80 | } | |
| 81 | ||
| 82 | /// Resolves a method executor for a given method name and argument types within the specified context and target | |
| 83 | /// object. This method attempts to find a matching executor for methods registered with a specific name and | |
| 84 | /// compatible argument types. | |
| 85 | /// | |
| 86 | /// @param context the evaluation context in which the method is being resolved, providing necessary state | |
| 87 | /// and configuration. | |
| 88 | /// @param targetObject the object on which the resolved method will be invoked. | |
| 89 | /// @param name the name of the method to resolve. | |
| 90 | /// @param argumentTypes a list of type descriptors representing the argument types of the method to | |
| 91 | /// resolve. | |
| 92 | /// | |
| 93 | /// @return the resolved [MethodExecutor] if a compatible method is found; otherwise, returns `null`. | |
| 94 | @Override | |
| 95 | @Nullable | |
| 96 | public MethodExecutor resolve( | |
| 97 | EvaluationContext context, | |
| 98 | Object targetObject, | |
| 99 | String name, | |
| 100 | List<TypeDescriptor> argumentTypes | |
| 101 | ) { | |
| 102 | var argumentClasses = argumentTypes.stream() | |
| 103 | .map(this::typeDescriptor2Class) | |
| 104 | .toList(); | |
| 105 |
1
1. resolve : replaced return value with null for pro/verron/officestamper/core/Invokers::resolve → KILLED |
return map.getOrDefault(name, emptyMap()) |
| 106 | .entrySet() | |
| 107 | .stream() | |
| 108 |
2
1. lambda$resolve$0 : replaced boolean return with true for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → TIMED_OUT 2. lambda$resolve$0 : replaced boolean return with false for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → KILLED |
.filter(entry -> entry.getKey() |
| 109 | .validate(argumentClasses)) | |
| 110 | .map(Entry::getValue) | |
| 111 | .findFirst() | |
| 112 | .orElse(null); | |
| 113 | } | |
| 114 | ||
| 115 | @SuppressWarnings("rawtypes") | |
| 116 | private Class typeDescriptor2Class(@Nullable TypeDescriptor typeDescriptor) { | |
| 117 | // When null, consider it as compatible with any type argument, so return Any.class placeholder | |
| 118 |
2
1. typeDescriptor2Class : negated conditional → TIMED_OUT 2. typeDescriptor2Class : replaced return value with null for pro/verron/officestamper/core/Invokers::typeDescriptor2Class → KILLED |
return typeDescriptor == null ? Any.class : typeDescriptor.getType(); |
| 119 | } | |
| 120 | ||
| 121 | /// This class represents a placeholder validating all other classes as possible candidate for validation. It is not | |
| 122 | /// supposed to be instantiated. | |
| 123 | private interface Any { | |
| 124 | ||
| 125 | } | |
| 126 | ||
| 127 | /// Represents argument types associated with method invocation. | |
| 128 | /// | |
| 129 | /// This record encapsulates a list of parameter types and provides a method to validate whether a list of target | |
| 130 | /// types matches the source types. | |
| 131 | /// | |
| 132 | /// The validation logic ensures that each target type is compatible with the corresponding source type. A type is | |
| 133 | /// compatible if it matches precisely or is assignable from the source type. Additionally, the `Any` class acts as | |
| 134 | /// a wildcard placeholder, making any type compatible. | |
| 135 | /// | |
| 136 | /// @param sourceTypes a list of parameter types representing the method's signature. | |
| 137 | public record Args(List<Class<?>> sourceTypes) { | |
| 138 | ||
| 139 | /// Validates if the provided list of classes matches the source types according to the compatibility rules. A | |
| 140 | /// type is considered compatible if it matches precisely or is assignable from the corresponding source type. | |
| 141 | /// Additionally, the `Any` class serves as a wildcard, making any type compatible. | |
| 142 | /// | |
| 143 | /// @param searchedTypes the list of classes to validate against the source types. | |
| 144 | /// | |
| 145 | /// @return true if all the searched classes are compatible with the source types; false otherwise. | |
| 146 | @SuppressWarnings("rawtypes") | |
| 147 | public boolean validate(List<Class> searchedTypes) { | |
| 148 |
2
1. validate : replaced boolean return with true for pro/verron/officestamper/core/Invokers$Args::validate → NO_COVERAGE 2. validate : negated conditional → KILLED |
if (searchedTypes.size() != sourceTypes.size()) return false; |
| 149 | ||
| 150 | var sourceTypesQ = new ArrayDeque<>(sourceTypes); | |
| 151 | var searchedTypesQ = new ArrayDeque<>(searchedTypes); | |
| 152 | var valid = true; | |
| 153 |
2
1. validate : negated conditional → TIMED_OUT 2. validate : negated conditional → KILLED |
while (!sourceTypesQ.isEmpty() && valid) { |
| 154 | Class<?> parameterType = sourceTypesQ.remove(); | |
| 155 | Class<?> searchedType = searchedTypesQ.remove(); | |
| 156 |
2
1. validate : negated conditional → TIMED_OUT 2. validate : negated conditional → KILLED |
valid = searchedType == Any.class || parameterType.isAssignableFrom(searchedType); |
| 157 | } | |
| 158 |
2
1. validate : replaced boolean return with true for pro/verron/officestamper/core/Invokers$Args::validate → TIMED_OUT 2. validate : replaced boolean return with false for pro/verron/officestamper/core/Invokers$Args::validate → KILLED |
return valid; |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 162 | /// Encapsulates a custom function as a method executor, allowing the execution of the function with a list of | |
| 163 | /// arguments in a given evaluation context. | |
| 164 | /// | |
| 165 | /// This class implements the `MethodExecutor` interface from the Spring Expression framework. | |
| 166 | private record CustomFunctionExecutor(Function<List<@Nullable Object>, Object> function) | |
| 167 | implements MethodExecutor { | |
| 168 | ||
| 169 | /// Executes the method with the provided evaluation context, target object, and arguments. | |
| 170 | /// | |
| 171 | /// The method applies the encapsulated function to the arguments and returns the result as a TypedValue. | |
| 172 | /// | |
| 173 | /// @param context the evaluation context in which the method is executed. | |
| 174 | /// @param target the target object on which the method is invoked, if applicable. | |
| 175 | /// @param arguments the arguments to be passed to the method during execution. | |
| 176 | /// | |
| 177 | /// @return the result of the method execution encapsulated in a TypedValue. | |
| 178 | @Override | |
| 179 | public TypedValue execute(EvaluationContext context, Object target, @Nullable Object... arguments) { | |
| 180 | var argumentList = Arrays.asList(arguments); | |
| 181 | var result = function.apply(argumentList); | |
| 182 |
1
1. execute : replaced return value with null for pro/verron/officestamper/core/Invokers$CustomFunctionExecutor::execute → KILLED |
return new TypedValue(result); |
| 183 | } | |
| 184 | } | |
| 185 | } | |
Mutations | ||
| 52 |
1.1 |
|
| 58 |
1.1 |
|
| 62 |
1.1 2.2 |
|
| 66 |
1.1 |
|
| 79 |
1.1 |
|
| 105 |
1.1 |
|
| 108 |
1.1 2.2 |
|
| 118 |
1.1 2.2 |
|
| 148 |
1.1 2.2 |
|
| 153 |
1.1 2.2 |
|
| 156 |
1.1 2.2 |
|
| 158 |
1.1 2.2 |
|
| 182 |
1.1 |