Invokers.java

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

Mutations

54

1.1
Location : streamInvokers
Killed by : pro.verron.officestamper.test.RegressionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.RegressionTests]/[method:test64()]
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → KILLED

60

1.1
Location : streamInvokers
Killed by : pro.verron.officestamper.test.RegressionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.RegressionTests]/[method:test64()]
replaced return value with Stream.empty for pro/verron/officestamper/core/Invokers::streamInvokers → KILLED

64

1.1
Location : streamInvokers
Killed by : pro.verron.officestamper.test.RegressionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.RegressionTests]/[method:test64()]
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.ResolutionTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.ResolutionTest]/[test-template:testStaticResolution(java.lang.String, boolean, boolean, boolean, java.lang.String, java.lang.String)]/[test-template-invocation:#6]
replaced return value with null for pro/verron/officestamper/core/Invokers::lambda$streamInvokers$0 → KILLED

80

1.1
Location : ofCustomFunction
Killed by : pro.verron.officestamper.test.ResolutionTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.ResolutionTest]/[test-template:testStaticResolution(java.lang.String, boolean, boolean, boolean, java.lang.String, java.lang.String)]/[test-template-invocation:#6]
replaced return value with null for pro/verron/officestamper/core/Invokers::ofCustomFunction → KILLED

106

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

109

1.1
Location : lambda$resolve$0
Killed by : pro.verron.officestamper.test.CustomFunctionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomFunctionTests]/[test-template:bifunctions(pro.verron.officestamper.test.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 : pro.verron.officestamper.test.DateFormatTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.DateFormatTests]/[test-template:features(pro.verron.officestamper.test.ContextFactory)]/[test-template-invocation:#2]
replaced boolean return with true for pro/verron/officestamper/core/Invokers::lambda$resolve$0 → KILLED

119

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.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:bifunctions(pro.verron.officestamper.test.ContextFactory)]/[test-template-invocation:#2]
replaced return value with null for pro/verron/officestamper/core/Invokers::typeDescriptor2Class → KILLED

151

1.1
Location : validate
Killed by : pro.verron.officestamper.test.DateFormatTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.DateFormatTests]/[test-template:features(pro.verron.officestamper.test.ContextFactory)]/[test-template-invocation:#2]
replaced boolean return with true for pro/verron/officestamper/core/Invokers$Args::validate → KILLED

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

156

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

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

159

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

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

161

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.CustomFunctionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomFunctionTests]/[test-template:bifunctions(pro.verron.officestamper.test.ContextFactory)]/[test-template-invocation:#2]
replaced boolean return with false for pro/verron/officestamper/core/Invokers$Args::validate → KILLED

181

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

Active mutators

Tests examined


Report generated by PIT 1.21.0