Invokers.java

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
Location : streamInvokersFromClass
Killed by : none
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokersFromClass → TIMED_OUT

58

1.1
Location : streamInvokers
Killed by : none
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → TIMED_OUT

62

1.1
Location : streamInvokers
Killed by : none
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → TIMED_OUT

2.2
Location : lambda$streamInvokers$0
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
replaced return value with null for pro/verron/officestamper/core/Invokers::lambda$streamInvokers$0 → KILLED

66

1.1
Location : streamInvokersFromCustomFunction
Killed by : none
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokersFromCustomFunction → TIMED_OUT

79

1.1
Location : ofCustomFunction
Killed by : pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.FailOnUnresolvedPlaceholderTest]/[test-template:fails(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
replaced return value with null for pro/verron/officestamper/core/Invokers::ofCustomFunction → KILLED

105

1.1
Location : resolve
Killed by : pro.verron.officestamper.test.CustomProcessorTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomProcessorTests]/[test-template:should_allow_custom_processors_injection(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
replaced return value with null for pro/verron/officestamper/core/Invokers::resolve → KILLED

108

1.1
Location : lambda$resolve$0
Killed by : pro.verron.officestamper.test.CustomProcessorTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomProcessorTests]/[test-template:should_allow_custom_processors_injection(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
replaced boolean return with false for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → KILLED

2.2
Location : lambda$resolve$0
Killed by : none
replaced boolean return with true for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → TIMED_OUT

118

1.1
Location : typeDescriptor2Class
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : typeDescriptor2Class
Killed by : pro.verron.officestamper.test.ProcessorDisplayIfTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.ProcessorDisplayIfTest]/[test-template:conditionalDisplayOfTableRowsTest(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
replaced return value with null for pro/verron/officestamper/core/Invokers::typeDescriptor2Class → KILLED

148

1.1
Location : validate
Killed by : none
replaced boolean return with true for pro/verron/officestamper/core/Invokers$Args::validate → NO_COVERAGE

2.2
Location : validate
Killed by : pro.verron.officestamper.test.CustomProcessorTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomProcessorTests]/[test-template:should_allow_custom_processors_injection(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
negated conditional → KILLED

153

1.1
Location : validate
Killed by : pro.verron.officestamper.test.CustomProcessorTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomProcessorTests]/[test-template:should_allow_custom_processors_injection(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
negated conditional → KILLED

2.2
Location : validate
Killed by : none
negated conditional → TIMED_OUT

156

1.1
Location : validate
Killed by : pro.verron.officestamper.test.ProcessorDisplayIfTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.ProcessorDisplayIfTest]/[test-template:conditionalDisplayOfTableRowsTest(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#2]
negated conditional → KILLED

2.2
Location : validate
Killed by : none
negated conditional → TIMED_OUT

158

1.1
Location : validate
Killed by : none
replaced boolean return with true for pro/verron/officestamper/core/Invokers$Args::validate → TIMED_OUT

2.2
Location : validate
Killed by : pro.verron.officestamper.test.CustomProcessorTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomProcessorTests]/[test-template:should_allow_custom_processors_injection(pro.verron.officestamper.test.utils.ContextFactory)]/[test-template-invocation:#1]
replaced boolean return with false for pro/verron/officestamper/core/Invokers$Args::validate → KILLED

182

1.1
Location : execute
Killed by : pro.verron.officestamper.test.CustomFunctionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomFunctionTests]/[test-template:trifunctions(pro.verron.officestamper.test.utils.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#9]
replaced return value with null for pro/verron/officestamper/core/Invokers$CustomFunctionExecutor::execute → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.0