createConfigurationProperty(FieldDeclaration,ConfigurationMetadataProperty)   F
last analyzed

Complexity

Conditions 10

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
c 0
b 0
f 0
cc 10
rs 3.4285

How to fix   Complexity   

Complexity

Complex classes like org.apereo.cas.configuration.metadata.ConfigurationMetadataGenerator.FieldVisitor.createConfigurationProperty(FieldDeclaration,ConfigurationMetadataProperty) 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
package org.apereo.cas.configuration.metadata;
2
3
import com.fasterxml.jackson.annotation.JsonInclude;
4
import com.fasterxml.jackson.core.PrettyPrinter;
5
import com.fasterxml.jackson.core.type.TypeReference;
6
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
7
import com.fasterxml.jackson.databind.DeserializationFeature;
8
import com.fasterxml.jackson.databind.ObjectMapper;
9
import com.github.javaparser.JavaParser;
10
import com.github.javaparser.ast.CompilationUnit;
11
import com.github.javaparser.ast.body.FieldDeclaration;
12
import com.github.javaparser.ast.body.VariableDeclarator;
13
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
14
import com.github.javaparser.ast.expr.Expression;
15
import com.github.javaparser.ast.expr.LiteralStringValueExpr;
16
import com.github.javaparser.ast.type.ClassOrInterfaceType;
17
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
18
import com.google.common.base.Predicate;
19
import org.apache.commons.lang3.ClassUtils;
20
import org.apache.commons.lang3.StringUtils;
21
import org.apereo.cas.configuration.model.core.authentication.PasswordPolicyProperties;
22
import org.apereo.cas.configuration.model.core.authentication.PrincipalTransformationProperties;
23
import org.apereo.cas.configuration.model.support.ldap.AbstractLdapProperties;
24
import org.apereo.cas.configuration.model.support.ldap.LdapSearchEntryHandlersProperties;
25
import org.apereo.cas.configuration.support.RequiredProperty;
26
import org.apereo.cas.configuration.support.RequiresModule;
27
import org.apereo.services.persondir.support.QueryType;
28
import org.apereo.services.persondir.util.CaseCanonicalizationMode;
29
import org.jooq.lambda.Unchecked;
30
import org.reflections.Reflections;
31
import org.reflections.scanners.SubTypesScanner;
32
import org.reflections.scanners.TypeElementsScanner;
33
import org.reflections.util.ClasspathHelper;
34
import org.reflections.util.ConfigurationBuilder;
35
import org.slf4j.Logger;
36
import org.slf4j.LoggerFactory;
37
import org.springframework.boot.bind.RelaxedNames;
38
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
39
import org.springframework.boot.configurationmetadata.ValueHint;
40
import org.springframework.util.ReflectionUtils;
41
42
import java.io.File;
43
import java.io.FileInputStream;
44
import java.io.InputStream;
45
import java.io.Serializable;
46
import java.util.Arrays;
47
import java.util.Collection;
48
import java.util.HashMap;
49
import java.util.HashSet;
50
import java.util.LinkedHashSet;
51
import java.util.List;
52
import java.util.Map;
53
import java.util.Set;
54
import java.util.regex.Matcher;
55
import java.util.regex.Pattern;
56
import java.util.stream.Collectors;
57
import java.util.stream.Stream;
58
import java.util.stream.StreamSupport;
59
60
/**
61
 * This is {@link ConfigurationMetadataGenerator}.
62
 * This class is invoked by the build during the finalization of the compile phase.
63
 * Its job is to scan the generated configuration metadata and produce metadata
64
 * for settings that the build process is unable to parse. Specifically,
65
 * this includes fields that are of collection type (indexed) where the inner type is an
66
 * externalized class.
67
 * <p>
68
 * Example:
69
 * {@code
70
 * private List<SomeClassProperties> list = new ArrayList<>()
71
 * }
72
 * The generator additionally adds hints to the metadata generated to indicate
73
 * required properties and modules.
74
 *
75
 * @author Misagh Moayyed
76
 * @since 5.2.0
77
 */
78
public class ConfigurationMetadataGenerator {
79
    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationMetadataGenerator.class);
80
    private static final Pattern PATTERN_GENERICS = Pattern.compile(".+\\<(.+)\\>");
81
    private static final Pattern NESTED_TYPE_PATTERN = Pattern.compile("java\\.util\\.\\w+<(org\\.apereo\\.cas\\..+)>");
82
83
    private final String buildDir;
84
    private final String sourcePath;
85
    private final Map<String, Class> cachedPropertiesClasses = new HashMap<>();
86
87
    public ConfigurationMetadataGenerator(final String buildDir, final String sourcePath) {
88
        this.buildDir = buildDir;
89
        this.sourcePath = sourcePath;
90
    }
91
92
    /**
93
     * Main.
94
     *
95
     * @param args the args
96
     * @throws Exception the exception
97
     */
98
    public static void main(final String[] args) throws Exception {
99
        new ConfigurationMetadataGenerator(args[0], args[1]).execute();
100
    }
101
102
    /**
103
     * Execute.
104
     *
105
     * @throws Exception the exception
106
     */
107
    public void execute() throws Exception {
108
        final File jsonFile = new File(buildDir, "classes/java/main/META-INF/spring-configuration-metadata.json");
109
        final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
110
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
111
112
        final TypeReference<Map<String, Set<ConfigurationMetadataProperty>>> values = new TypeReference<Map<String, Set<ConfigurationMetadataProperty>>>() {
113
        };
114
        final Map<String, Set> jsonMap = mapper.readValue(jsonFile, values);
115
        final Set<ConfigurationMetadataProperty> properties = jsonMap.get("properties");
116
        final Set<ConfigurationMetadataProperty> groups = jsonMap.get("groups");
117
118
        final Set<ConfigurationMetadataProperty> collectedProps = new HashSet<>();
119
        final Set<ConfigurationMetadataProperty> collectedGroups = new HashSet<>();
120
121
        properties.stream()
122
                .filter(p -> NESTED_TYPE_PATTERN.matcher(p.getType()).matches())
123
                .forEach(Unchecked.consumer(p -> {
124
                    final Matcher matcher = NESTED_TYPE_PATTERN.matcher(p.getType());
125
                    final boolean indexBrackets = matcher.matches();
126
                    final String typeName = matcher.group(1);
127
                    final String typePath = buildTypeSourcePath(typeName);
128
129
                    parseCompilationUnit(collectedProps, collectedGroups, p, typePath, typeName, indexBrackets);
130
131
                }));
132
133
        properties.addAll(collectedProps);
134
        groups.addAll(collectedGroups);
135
136
        final Set<ConfigurationMetadataHint> hints = processHints(properties, groups);
137
138
        jsonMap.put("properties", properties);
139
        jsonMap.put("groups", groups);
140
        jsonMap.put("hints", hints);
141
142
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
143
        final PrettyPrinter pp = new DefaultPrettyPrinter();
144
        mapper.writer(pp).writeValue(jsonFile, jsonMap);
145
    }
146
147
    private String buildTypeSourcePath(final String type) {
148
        final String newName = type.replace(".", File.separator);
149
        return sourcePath + "/src/main/java/" + newName + ".java";
150
    }
151
152
    private void parseCompilationUnit(final Set<ConfigurationMetadataProperty> collectedProps,
153
                                      final Set<ConfigurationMetadataProperty> collectedGroups,
154
                                      final ConfigurationMetadataProperty p,
155
                                      final String typePath,
156
                                      final String typeName,
157
                                      final boolean indexNameWithBrackets) {
158
159
        try (InputStream is = new FileInputStream(typePath)) {
160
            final CompilationUnit cu = JavaParser.parse(is);
161
            new FieldVisitor(collectedProps, collectedGroups, indexNameWithBrackets, typeName).visit(cu, p);
162
        } catch (final Exception e) {
163
            throw new RuntimeException(e.getMessage(), e);
164
        }
165
    }
166
167
    private class FieldVisitor extends VoidVisitorAdapter<ConfigurationMetadataProperty> {
168
        private final Set<ConfigurationMetadataProperty> properties;
169
        private final Set<ConfigurationMetadataProperty> groups;
170
171
        private final String parentClass;
172
        private final boolean indexNameWithBrackets;
173
174
        FieldVisitor(final Set<ConfigurationMetadataProperty> properties, final Set<ConfigurationMetadataProperty> groups,
175
                     final boolean indexNameWithBrackets, final String clazz) {
176
            this.properties = properties;
177
            this.groups = groups;
178
            this.indexNameWithBrackets = indexNameWithBrackets;
179
            this.parentClass = clazz;
180
        }
181
182
        @Override
183
        public void visit(final FieldDeclaration field, final ConfigurationMetadataProperty property) {
184
            if (field.getVariables().isEmpty()) {
185
                throw new IllegalArgumentException("Field " + field + " has no variable definitions");
186
            }
187
188
            if (field.getJavadoc().isPresent()) {
189
                final ConfigurationMetadataProperty prop = createConfigurationProperty(field, property);
190
                processNestedClassOrInterfaceTypeIfNeeded(field, prop);
191
            } else {
192
                final VariableDeclarator var = field.getVariable(0);
193
                if (!var.getNameAsString().matches("serialVersionUID")) {
194
                    LOGGER.error("Field " + field + " has no Javadoc defined");
195
                }
196
            }
197
        }
198
199
        private ConfigurationMetadataProperty createConfigurationProperty(final FieldDeclaration n,
200
                                                                          final ConfigurationMetadataProperty arg) {
201
            final VariableDeclarator variable = n.getVariables().get(0);
202
            final String name = StreamSupport.stream(RelaxedNames.forCamelCase(variable.getNameAsString()).spliterator(), false)
203
                    .map(Object::toString)
204
                    .findFirst()
205
                    .orElse(variable.getNameAsString());
206
207
            final String indexedGroup = arg.getName().concat(indexNameWithBrackets ? "[]" : StringUtils.EMPTY);
208
            final String indexedName = indexedGroup.concat(".").concat(name);
209
210
            final ConfigurationMetadataProperty prop = new ConfigurationMetadataProperty();
211
            final String description = n.getJavadoc().get().getDescription().toText();
212
            prop.setDescription(description);
213
            prop.setShortDescription(StringUtils.substringBefore(description, "."));
214
            prop.setName(indexedName);
215
            prop.setId(indexedName);
216
217
218
            final String elementType = n.getElementType().asString();
219
            if (elementType.equals(String.class.getSimpleName())
220
                    || elementType.equals(Integer.class.getSimpleName())
221
                    || elementType.equals(Long.class.getSimpleName())
222
                    || elementType.equals(Double.class.getSimpleName())
223
                    || elementType.equals(Float.class.getSimpleName())) {
224
                prop.setType("java.lang." + elementType);
225
            } else {
226
                prop.setType(elementType);
227
            }
228
229
            if (variable.getInitializer().isPresent()) {
230
                final Expression exp = variable.getInitializer().get();
231
                if (exp instanceof LiteralStringValueExpr) {
232
                    prop.setDefaultValue(((LiteralStringValueExpr) exp).getValue());
233
                } else if (exp instanceof BooleanLiteralExpr) {
234
                    prop.setDefaultValue(((BooleanLiteralExpr) exp).getValue());
235
                }
236
            }
237
            properties.add(prop);
238
239
            final ConfigurationMetadataProperty grp = new ConfigurationMetadataProperty();
240
            grp.setId(indexedGroup);
241
            grp.setName(indexedGroup);
242
            grp.setType(this.parentClass);
243
            groups.add(grp);
244
245
            return prop;
246
        }
247
248
        private void processNestedClassOrInterfaceTypeIfNeeded(final FieldDeclaration n, final ConfigurationMetadataProperty prop) {
249
            if (n.getElementType() instanceof ClassOrInterfaceType) {
250
                final ClassOrInterfaceType type = (ClassOrInterfaceType) n.getElementType();
251
                if (!shouldTypeBeExcluded(type)) {
252
                    final Class clz = locatePropertiesClassForType(type);
253
                    if (clz != null && !clz.isMemberClass()) {
254
                        final String typePath = buildTypeSourcePath(clz.getName());
255
                        parseCompilationUnit(properties, groups, prop, typePath, clz.getName(), false);
256
                    }
257
                }
258
            }
259
        }
260
261
        private boolean shouldTypeBeExcluded(final ClassOrInterfaceType type) {
262
            return type.getNameAsString().matches(String.class.getSimpleName() + "|"
263
                    + Integer.class.getSimpleName() + "|"
264
                    + Double.class.getSimpleName() + "|"
265
                    + Long.class.getSimpleName() + "|"
266
                    + Float.class.getSimpleName() + "|"
267
                    + PrincipalTransformationProperties.CaseConversion.class.getSimpleName() + "|"
268
                    + QueryType.class.getSimpleName() + "|"
269
                    + AbstractLdapProperties.LdapType.class.getSimpleName() + "|"
270
                    + CaseCanonicalizationMode.class.getSimpleName() + "|"
271
                    + PasswordPolicyProperties.PasswordPolicyHandlingOptions.class.getSimpleName() + "|"
272
                    + LdapSearchEntryHandlersProperties.SearchEntryHandlerTypes.class.getSimpleName() + "|"
273
                    + Map.class.getSimpleName() + "|"
274
                    + List.class.getSimpleName() + "|"
275
                    + Set.class.getSimpleName());
276
        }
277
278
    }
279
280
    private Class locatePropertiesClassForType(final ClassOrInterfaceType type) {
281
        if (cachedPropertiesClasses.containsKey(type.getNameAsString())) {
282
            return cachedPropertiesClasses.get(type.getNameAsString());
283
        }
284
285
        final Predicate<String> filterInputs = s -> s.contains(type.getNameAsString());
286
        final Predicate<String> filterResults = s -> s.endsWith(type.getNameAsString());
287
        final String packageName = ConfigurationMetadataGenerator.class.getPackage().getName();
288
        final Reflections reflections =
289
                new Reflections(new ConfigurationBuilder()
290
                        .filterInputsBy(filterInputs)
291
                        .setUrls(ClasspathHelper.forPackage(packageName))
292
                        .setScanners(new TypeElementsScanner()
293
                                        .includeFields(false)
294
                                        .includeMethods(false)
295
                                        .includeAnnotations(false)
296
                                        .filterResultsBy(filterResults),
297
                                new SubTypesScanner(false)));
298
        final Class clz = reflections.getSubTypesOf(Serializable.class).stream()
299
                .filter(c -> c.getSimpleName().equalsIgnoreCase(type.getNameAsString()))
300
                .findFirst()
301
                .orElseThrow(() -> new IllegalArgumentException("Cant locate class for " + type.getNameAsString()));
302
        cachedPropertiesClasses.put(type.getNameAsString(), clz);
303
        return clz;
304
    }
305
306
    private Set<ConfigurationMetadataHint> processHints(final Collection<ConfigurationMetadataProperty> props,
307
                                                        final Collection<ConfigurationMetadataProperty> groups) {
308
309
        final Set<ConfigurationMetadataHint> hints = new LinkedHashSet<>();
310
311
        for (final ConfigurationMetadataProperty entry : props) {
312
            try {
313
                final String propName = StringUtils.substringAfterLast(entry.getName(), ".");
314
                final String groupName = StringUtils.substringBeforeLast(entry.getName(), ".");
315
                final ConfigurationMetadataProperty grp = groups
316
                        .stream()
317
                        .filter(g -> g.getName().equalsIgnoreCase(groupName))
318
                        .findFirst()
319
                        .orElseThrow(() -> new IllegalArgumentException("Cant locate group " + groupName));
320
321
                final Matcher matcher = PATTERN_GENERICS.matcher(grp.getType());
322
                final String className = matcher.find() ? matcher.group(1) : grp.getType();
323
                final Class clazz = ClassUtils.getClass(className);
324
325
326
                final ConfigurationMetadataHint hint = new ConfigurationMetadataHint();
327
                hint.setName(entry.getName());
328
329
                if (clazz.isAnnotationPresent(RequiresModule.class)) {
330
                    final RequiresModule annotation = Arrays.stream(clazz.getAnnotations())
331
                            .filter(a -> a.annotationType().equals(RequiresModule.class))
332
                            .findFirst()
333
                            .map(RequiresModule.class::cast)
334
                            .get();
335
                    final ValueHint valueHint = new ValueHint();
336
                    valueHint.setValue(Stream.of(RequiresModule.class.getName(), annotation.automated()).collect(Collectors.toList()));
337
                    valueHint.setDescription(annotation.name());
338
                    hint.getValues().add(valueHint);
339
                }
340
341
                final boolean foundRequiredProperty = StreamSupport.stream(RelaxedNames.forCamelCase(propName).spliterator(), false)
342
                        .map(n -> ReflectionUtils.findField(clazz, n))
343
                        .anyMatch(f -> f != null && f.isAnnotationPresent(RequiredProperty.class));
344
345
                if (foundRequiredProperty) {
346
                    final ValueHint valueHint = new ValueHint();
347
                    valueHint.setValue(RequiredProperty.class.getName());
348
                    hint.getValues().add(valueHint);
349
                }
350
351
                if (!hint.getValues().isEmpty()) {
352
                    hints.add(hint);
353
                }
354
            } catch (final Exception e) {
355
                LOGGER.error(e.getMessage(), e);
356
            }
357
        }
358
        return hints;
359
    }
360
}
361