Completed
Branch master (dc3042)
by Zaahid
03:52
created

findCandidateFunction(List)   C

Complexity

Conditions 11

Size

Total Lines 39
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
cc 11
eloc 34
c 0
b 0
f 0
dl 0
loc 39
ccs 0
cts 31
cp 0
crap 132
rs 5.4

How to fix   Complexity   

Complexity

Complex classes like com.strider.datadefender.requirement.plan.Function.findCandidateFunction(List) often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/*
2
 * Copyright 2014, Armenak Grigoryan, and individual contributors as indicated
3
 * by the @authors tag. See the copyright.txt in the distribution for a
4
 * full listing of individual contributors.
5
 *
6
 * This is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU Lesser General Public License as
8
 * published by the Free Software Foundation; either version 2.1 of
9
 * the License, or (at your option) any later version.
10
 *
11
 * This software is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
 * Lesser General Public License for more details.
15
 */
16
package com.strider.datadefender.requirement.plan;
17
18
import com.strider.datadefender.functions.NamedParameter;
19
import com.strider.datadefender.requirement.TypeConverter;
20
import com.strider.datadefender.requirement.registry.ClassAndFunctionRegistry;
21
import com.strider.datadefender.requirement.registry.RequirementFunction;
22
23
import java.lang.reflect.InvocationTargetException;
24
import java.lang.reflect.Method;
25
import java.lang.reflect.Modifier;
26
import java.sql.SQLException;
27
import java.util.ArrayList;
28
import java.util.Arrays;
29
import java.util.Comparator;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.stream.Collectors;
33
34
import javax.xml.bind.annotation.XmlAccessType;
35
import javax.xml.bind.annotation.XmlAccessorType;
36
import javax.xml.bind.annotation.XmlAttribute;
37
import javax.xml.bind.annotation.XmlElement;
38
39
import org.apache.commons.collections4.CollectionUtils;
40
import org.apache.commons.lang3.ClassUtils;
41
import org.apache.commons.lang3.StringUtils;
42
43
import lombok.AccessLevel;
44
import lombok.Data;
45
import lombok.Getter;
46
import lombok.Setter;
47
import lombok.extern.log4j.Log4j2;
48
49
/**
50
 *
51
 * @author Zaahid Bateson
52
 */
53
@Log4j2
54
@Data
55
@XmlAccessorType(XmlAccessType.NONE)
56
public class Function implements Invokable {
57
58
    @Setter(AccessLevel.NONE)
59
    @XmlAttribute(name = "name")
60
    private String functionName;
61
62
    private Method function;
63
64
    @XmlAttribute(name = "combiner-glue")
65
    private String combinerGlue;
66
67
    private Object combinerGlueObject;
68
69
    @XmlElement(name = "argument")
70
    private List<Argument> arguments;
71
72
    private boolean isCombinerFunction = false;
73
74
    @Getter(AccessLevel.NONE)
75
    @Setter(AccessLevel.NONE)
76
    private ClassAndFunctionRegistry registry;
77
78
    public Function() {
79
        this(ClassAndFunctionRegistry.singleton());
80
    }
81
82
    public Function(ClassAndFunctionRegistry registry) {
83
        this.registry = registry;
84
    }
85
86
    public Function(String functionName, boolean isCombinerFunction) {
87
        this();
88
        this.functionName = functionName;
89
        this.isCombinerFunction = isCombinerFunction;
90
    }
91
92
    /**
93
     * Setter for 'Function' element.
94
     *
95
     * @param fn
96
     */
97
    public void setFunction(Method fn) {
98
        function = fn;
99
        functionName = fn.getName();
100
    }
101
102
    /**
103
     * Returns true if the underlying method is static
104
     */
105
    public boolean isStatic() {
106
        return Modifier.isStatic(function.getModifiers());
107
    }
108
109
    /**
110
     * Looks for a class/method in the passed Function parameter in the form
111
     * com.package.Class#methodName, com.package.Class.methodName, or
112
     * com.package.Class::methodName.
113
     *
114
     * @return
115
     * @throws ClassNotFoundException
116
     */
117
    private List<Method> getFunctionCandidates(Class<?> returnType) 
118
        throws ClassNotFoundException {
119
120
        int index = StringUtils.lastIndexOfAny(functionName, "#", ".", "::");
121
        if (index == -1) {
122
            throw new IllegalArgumentException(
123
                "Function element is empty or incomplete: " + functionName
124
            );
125
        }
126
127
        String cn = functionName.substring(0, index);
128
        String fn = StringUtils.stripStart(functionName.substring(index), "#.:");
129
        int argCount = CollectionUtils.size(arguments);
130
131
        log.debug("Looking for function in class {} with name {} and {} parameters", cn, fn, argCount);
132
        Class<?> clazz = registry.getClassForName(cn);
133
        List<Method> methods = Arrays.asList(clazz.getMethods());
134
135
        return methods
136
            .stream()
137
            .filter((m) -> {
138
                if (!StringUtils.equals(fn, m.getName()) || !Modifier.isPublic(m.getModifiers())) {
139
                    return false;
140
                }
141
                final int ac = (!isCombinerFunction || Modifier.isStatic(m.getModifiers())) ? argCount : 1;
142
                log.debug(
143
                    "Candidate function {} needs {} parameters and gives {} return type, "
144
                    + "looking for {} arguments and {} return type",
145
                    () -> m.getName(),
146
                    () -> m.getParameterCount(),
147
                    () -> m.getReturnType(),
148
                    () -> ac,
149
                    () -> returnType
150
                );
151
                return (m.getParameterCount() == ac
152
                    && TypeConverter.isConvertible(m.getReturnType(), returnType));
153
            })
154
            .collect(Collectors.toList());
155
    }
156
157
    /**
158
     * 
159
     * @param type 
160
     */
161
    private void initializeCombinerGlue(Class<?> type)
162
        throws InstantiationException,
163
        IllegalAccessException,
164
        IllegalArgumentException,
165
        InvocationTargetException {
166
        if (!isCombinerFunction && combinerGlue != null) {
167
            log.debug("Converting combinerGlue {} to object of type {}", combinerGlue, type);
168
            combinerGlueObject = TypeConverter.convert(combinerGlue, type);
169
        }
170
    }
171
172
    /**
173
     * Finds a method in the passed candidates with parameters matching the
174
     * arguments assigned to the current object, and returns it, or null if not
175
     * found.
176
     *
177
     * @param candidates
178
     * @return
179
     */
180
    private Method findCandidateFunction(List<Method> candidates) {
181
        final Map<String, Argument> mappedArgs = CollectionUtils.emptyIfNull(arguments).stream()
182
            .collect(Collectors.toMap(Argument::getName, (o) -> o, (x, y) -> y));
183
        return candidates.stream().filter((m) -> {
184
            int index = -1;
185
            for (java.lang.reflect.Parameter p : m.getParameters()) {
186
                ++index;
187
                Argument arg = arguments.get(index);
188
                NamedParameter named = p.getAnnotation(NamedParameter.class);
189
                if (named != null && mappedArgs.containsKey(named.value())) {
190
                    arg = mappedArgs.get(named.value());
191
                } else if (mappedArgs.containsKey(p.getName())) {
192
                    arg = mappedArgs.get(p.getName());
193
                }
194
                if (arg == null || !TypeConverter.isConvertible(p.getType(), arg.getType())) {
195
                    return false;
196
                }
197
            }
198
            return true;
199
        }).sorted((a, b) -> {
200
            int score = 0;
201
            java.lang.reflect.Parameter[] aps = a.getParameters();
202
            java.lang.reflect.Parameter[] bps = b.getParameters();
203
            log.debug("Sorting method: {}", a.getName());
204
            for (int i = 0; i < aps.length; ++i) {
205
                Argument arg = arguments.get(i);
206
                NamedParameter named = aps[i].getAnnotation(NamedParameter.class);
207
                if (named != null && mappedArgs.containsKey(named.value())) {
208
                    arg = mappedArgs.get(named.value());
209
                } else if (mappedArgs.containsKey(aps[i].getName())) {
210
                    arg = mappedArgs.get(aps[i].getName());
211
                }
212
                int s = TypeConverter.compareConversion(arg.getType(), aps[i].getType(), bps[i].getType());
213
                log.debug("Comparing arguments for sorting: {} with: {} and: {}, result: {}", arg.getType(), aps[i].getType(), bps[i].getType(), s);
214
                score += s;
215
            }
216
            log.debug("Score between {}, {}: {}", a, b, score);
217
            return score;
218
        }).findFirst().orElse(null);
219
    }
220
221
    /**
222
     * Specialized function finder for a combiner that looks for either a single
223
     * parameter for a non-static function that would be run on the first
224
     * argument, or a two-parameter static function with compatible types.
225
     *
226
     * @param candidates
227
     * @return
228
     */
229
    private Method findCombinerCandidateFunction(List<Method> candidates) {
230
        return candidates.stream().filter((m) -> {
231
            boolean isStatic = Modifier.isStatic(m.getModifiers());
232
            int count = m.getParameterCount();
233
            if (count == 0 || (isStatic && count != 2) || (!isStatic && count != 1)) {
234
                return false;
235
            }
236
            int index = -1;
237
            if (!isStatic && !RequirementFunction.class.isAssignableFrom(m.getDeclaringClass())) {
238
                ++index;
239
                if (!TypeConverter.isConvertible(arguments.get(index).getType(), m.getDeclaringClass())) {
240
                    return false;
241
                }
242
            }
243
            for (java.lang.reflect.Parameter p : m.getParameters()) {
244
                ++index;
245
                Argument arg = arguments.get(index);
246
                if (!TypeConverter.isConvertible(p.getType(), arg.getType())) {
247
                    return false;
248
                }
249
            }
250
            return true;
251
        }).findFirst().orElse(null);
252
    }
253
254
    /**
255
     * Uses functionName and arguments to find the method to associate with
256
     * 'Function'.
257
     *
258
     * @param returnType
259
     */
260
    Method initialize(Class<?> returnType)
261
        throws ClassNotFoundException,
262
        InstantiationException,
263
        IllegalAccessException,
264
        IllegalArgumentException,
265
        InvocationTargetException {
266
267
        log.debug("Initializing function {}", functionName);
268
        initializeCombinerGlue(returnType);
269
        List<Method> candidates = getFunctionCandidates(returnType);
270
        log.debug(
271
            "Found method candidates: {}",
272
            () -> CollectionUtils.emptyIfNull(candidates).stream()
273
                .map((m) -> m.getName() + " " + m.getParameterCount())
274
                .collect(Collectors.toList())
275
        );
276
        if (!isCombinerFunction) {
277
            function = findCandidateFunction(candidates);
278
        } else {
279
            function = findCombinerCandidateFunction(candidates);
280
        }
281
282
        log.debug("Function references method: {}", () -> (function == null) ? "null" : function);
283
        // could try and sort returned functions if more than one based on
284
        // "best selection" for argument/parameter types
285
        if (function == null) {
286
            throw new IllegalArgumentException("Function maching signature and arguments not found");
287
        }
288
        return function;
289
    }
290
291
    /**
292
     * Runs the function referenced by the "Function" element and returns its
293
     * value.
294
     *
295
     * @param lastValue
296
     * @return
297
     * @throws SQLException
298
     * @throws IllegalAccessException
299
     * @throws InvocationTargetException
300
     */
301
    @Override
302
    public Object invoke(Object lastValue)
303
        throws SQLException,
304
        IllegalAccessException,
305
        InvocationTargetException,
306
        InstantiationException {
307
308
        log.debug("Function declaring class: {}", function.getDeclaringClass());
309
        log.debug("Function: {}", function);
310
        ClassAndFunctionRegistry registry = ClassAndFunctionRegistry.singleton();
311
        Object ob = registry.getFunctionsSingleton(function.getDeclaringClass());
312
        if (
313
            ob == null
314
            && lastValue != null
315
            && !Modifier.isStatic(function.getModifiers())
316
            && !RequirementFunction.class.isAssignableFrom(function.getDeclaringClass())
317
            && TypeConverter.isConvertible(lastValue.getClass(), function.getDeclaringClass())
318
        ) {
319
            ob = TypeConverter.convert(lastValue, function.getDeclaringClass());
320
        }
321
322
        java.lang.reflect.Parameter[] parameters = function.getParameters();
323
        List<Object> fnArguments = new ArrayList<>();
324
        final Map<String, Argument> mappedArgs = CollectionUtils.emptyIfNull(arguments).stream()
325
            .collect(Collectors.toMap(Argument::getName, (o) -> o, (x, y) -> x));
326
        // because getValue(rs) throws exceptions, can't use stream map
327
        // lambda function
328
        int index = -1;
329
        for (java.lang.reflect.Parameter p : parameters) {
330
            ++index;
331
            log.debug("Looking for argument {} in {} or {} in {}", p.getName(), mappedArgs, index, arguments);
332
            Argument arg = arguments.get(index);
333
            NamedParameter named = p.getAnnotation(NamedParameter.class);
334
            if (named != null && mappedArgs.containsKey(named.value())) {
335
                arg = mappedArgs.get(named.value());
336
            } else if (mappedArgs.containsKey(p.getName())) {
337
                arg = mappedArgs.get(p.getName());
338
            }
339
            fnArguments.add(TypeConverter.convert(
340
                arg.getValue(lastValue),
341
                p.getType()
342
            ));
343
        }
344
        final Object fnOb = ob;
345
        log.debug("invoking: {} on: {} with: ({})",
346
            () -> function.getName(),
347
            () -> fnOb,
348
            () -> fnArguments.toString()
349
        );
350
        return function.invoke(fnOb, fnArguments.toArray());
351
    }
352
}
353