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

Mutations

57

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

63

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

67

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

85

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

98

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

101

1.1
Location : lambda$resolve$1
Killed by : pro.verron.officestamper.test.CustomFunctionTests.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.CustomFunctionTests]/[test-template:trifunctions(pro.verron.officestamper.test.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#8]
replaced boolean return with false for pro/verron/officestamper/core/Invokers::lambda$resolve$1 → KILLED

2.2
Location : lambda$resolve$1
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$1 → KILLED

110

1.1
Location : typeDescriptor2Class
Killed by : pro.verron.officestamper.test.ConditionalDisplayTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.test.ConditionalDisplayTest]/[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:trifunctions(pro.verron.officestamper.test.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#8]
replaced return value with null for pro/verron/officestamper/core/Invokers::typeDescriptor2Class → KILLED

137

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:trifunctions(pro.verron.officestamper.test.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#8]
negated conditional → KILLED

142

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

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

145

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.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#8]
negated conditional → KILLED

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

147

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:trifunctions(pro.verron.officestamper.test.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#8]
replaced boolean return with false for pro/verron/officestamper/core/Invokers$Args::validate → KILLED

172

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.ContextFactory, java.lang.String, java.lang.String)]/[test-template-invocation:#8]
replaced return value with null for pro/verron/officestamper/core/Invokers$CustomFunctionExecutor::execute → KILLED

Active mutators

Tests examined


Report generated by PIT 1.20.0