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 → KILLED
        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 → KILLED
        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 → KILLED
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 → KILLED
        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
    /// @return the invoker.
74
    public static Invoker ofCustomFunction(CustomFunction cf) {
75
        var cfName = cf.name();
76
        var cfArgs = new Args(cf.parameterTypes());
77
        var cfExecutor = new CustomFunctionExecutor(cf.function());
78 1 1. ofCustomFunction : replaced return value with null for pro/verron/officestamper/core/Invokers::ofCustomFunction → KILLED
        return new Invoker(cfName, cfArgs, cfExecutor);
79
    }
80
81
    /// Resolves a method executor for a given method name and argument types within the specified context and target
82
    /// object. This method attempts to find a matching executor for methods registered with a specific name and
83
    /// compatible argument types.
84
    ///
85
    /// @param context the evaluation context in which the method is being resolved, providing necessary state
86
    ///         and configuration.
87
    /// @param targetObject the object on which the resolved method will be invoked.
88
    /// @param name the name of the method to resolve.
89
    /// @param argumentTypes a list of type descriptors representing the argument types of the method to
90
    ///         resolve.
91
    ///
92
    /// @return the resolved [MethodExecutor] if a compatible method is found; otherwise, returns `null`.
93
    @Override
94
    @Nullable
95
    public MethodExecutor resolve(
96
            EvaluationContext context,
97
            Object targetObject,
98
            String name,
99
            List<TypeDescriptor> argumentTypes
100
    ) {
101
        var argumentClasses = argumentTypes.stream()
102
                                           .map(this::typeDescriptor2Class)
103
                                           .toList();
104 1 1. resolve : replaced return value with null for pro/verron/officestamper/core/Invokers::resolve → KILLED
        return map.getOrDefault(name, emptyMap())
105
                  .entrySet()
106
                  .stream()
107 2 1. lambda$resolve$0 : replaced boolean return with true for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → SURVIVED
2. lambda$resolve$0 : replaced boolean return with false for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → KILLED
                  .filter(entry -> entry.getKey()
108
                                        .validate(argumentClasses))
109
                  .map(Entry::getValue)
110
                  .findFirst()
111
                  .orElse(null);
112
    }
113
114
    @SuppressWarnings("rawtypes")
115
    private Class typeDescriptor2Class(@Nullable TypeDescriptor typeDescriptor) {
116
        // When null, consider it as compatible with any type argument, so return Any.class placeholder
117 2 1. typeDescriptor2Class : negated conditional → KILLED
2. typeDescriptor2Class : replaced return value with null for pro/verron/officestamper/core/Invokers::typeDescriptor2Class → KILLED
        return typeDescriptor == null ? Any.class : typeDescriptor.getType();
118
    }
119
120
    /// This class represents a placeholder validating all other classes as possible candidate for validation. It is not
121
    /// supposed to be instantiated.
122
    private interface Any {
123
124
    }
125
126
    /// Represents argument types associated with method invocation.
127
    ///
128
    /// This record encapsulates a list of parameter types and provides a method to validate whether a list of target
129
    /// types matches the source types.
130
    ///
131
    /// The validation logic ensures that each target type is compatible with the corresponding source type. A type is
132
    /// compatible if it matches precisely or is assignable from the source type. Additionally, the [Any] class acts as
133
    /// a wildcard placeholder, making any type compatible.
134
    ///
135
    /// @param sourceTypes a list of parameter types representing the method's signature.
136
    public record Args(List<Class<?>> sourceTypes) {
137
138
        /// Validates if the provided list of classes matches the source types according to the compatibility rules. A
139
        /// type is considered compatible if it matches precisely or is assignable from the corresponding source type.
140
        /// Additionally, the [Any] class serves as a wildcard, making any type compatible.
141
        ///
142
        /// @param searchedTypes the list of classes to validate against the source types.
143
        /// @return true if all the searched classes are compatible with the source types; false otherwise.
144
        @SuppressWarnings("rawtypes")
145
        public boolean validate(List<Class> searchedTypes) {
146 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;
147
148
            var sourceTypesQ = new ArrayDeque<>(sourceTypes);
149
            var searchedTypesQ = new ArrayDeque<>(searchedTypes);
150
            var valid = true;
151 2 1. validate : negated conditional → TIMED_OUT
2. validate : negated conditional → KILLED
            while (!sourceTypesQ.isEmpty() && valid) {
152
                Class<?> parameterType = sourceTypesQ.remove();
153
                Class<?> searchedType = searchedTypesQ.remove();
154 2 1. validate : negated conditional → SURVIVED
2. validate : negated conditional → KILLED
                valid = searchedType == Any.class || parameterType.isAssignableFrom(searchedType);
155
            }
156 2 1. validate : replaced boolean return with true for pro/verron/officestamper/core/Invokers$Args::validate → SURVIVED
2. validate : replaced boolean return with false for pro/verron/officestamper/core/Invokers$Args::validate → KILLED
            return valid;
157
        }
158
    }
159
160
    /// Encapsulates a custom function as a method executor, allowing the execution of the function with a list of
161
    /// arguments in a given evaluation context.
162
    ///
163
    /// This class implements the [MethodExecutor] interface from the Spring Expression framework.
164
    private record CustomFunctionExecutor(Function<List<@Nullable Object>, Object> function)
165
            implements MethodExecutor {
166
167
        /// Executes the method with the provided evaluation context, target object, and arguments.
168
        ///
169
        /// The method applies the encapsulated function to the arguments and returns the result as a TypedValue.
170
        ///
171
        /// @param context the evaluation context in which the method is executed.
172
        /// @param target the target object on which the method is invoked, if applicable.
173
        /// @param arguments the arguments to be passed to the method during execution.
174
        /// @return the result of the method execution encapsulated in a TypedValue.
175
        @Override
176
        public TypedValue execute(EvaluationContext context, Object target, @Nullable Object... arguments) {
177
            var argumentList = Arrays.asList(arguments);
178
            var result = function.apply(argumentList);
179 1 1. execute : replaced return value with null for pro/verron/officestamper/core/Invokers$CustomFunctionExecutor::execute → KILLED
            return new TypedValue(result);
180
        }
181
    }
182
}

Mutations

52

1.1
Location : streamInvokersFromClass
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:#2]
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokersFromClass → KILLED

58

1.1
Location : streamInvokers
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:#2]
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → KILLED

62

1.1
Location : streamInvokers
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:#2]
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → KILLED

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:#2]
replaced return value with null for pro/verron/officestamper/core/Invokers::lambda$streamInvokers$0 → KILLED

66

1.1
Location : streamInvokersFromCustomFunction
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:#12]
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokersFromCustomFunction → KILLED

78

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:#2]
replaced return value with null for pro/verron/officestamper/core/Invokers::ofCustomFunction → KILLED

104

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:#2]
replaced return value with null for pro/verron/officestamper/core/Invokers::resolve → KILLED

107

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:#2]
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 → SURVIVED
Covering tests

117

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

2.2
Location : typeDescriptor2Class
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:#12]
replaced return value with null for pro/verron/officestamper/core/Invokers::typeDescriptor2Class → KILLED

146

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:#2]
negated conditional → KILLED

151

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:#2]
negated conditional → KILLED

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

154

1.1
Location : validate
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:#12]
negated conditional → KILLED

2.2
Location : validate
Killed by : none
negated conditional → SURVIVED
Covering tests

156

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

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:#2]
replaced boolean return with false for pro/verron/officestamper/core/Invokers$Args::validate → KILLED

179

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:#11]
replaced return value with null for pro/verron/officestamper/core/Invokers$CustomFunctionExecutor::execute → KILLED

Active mutators

Tests examined


Report generated by PIT 1.25.5 support