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
|
|
|
|