1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.tools.plugin.extractor.annotations.converter;
20
21 import java.net.URI;
22 import java.net.URISyntaxException;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.stream.Collectors;
30
31 import com.github.javaparser.ast.ImportDeclaration;
32 import com.github.javaparser.ast.Node;
33 import com.github.javaparser.ast.body.FieldDeclaration;
34 import com.github.javaparser.ast.body.TypeDeclaration;
35 import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration;
36 import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
37 import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
38 import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration;
39 import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
40 import com.github.javaparser.resolution.types.ResolvedReferenceType;
41 import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel;
42 import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
43 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
44 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
45 import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
46 import org.apache.maven.tools.plugin.javadoc.JavadocReference;
47
48
49 public class JavaClassConverterContext implements ConverterContext {
50
51
52 private final TypeDeclaration<?> mojoClass;
53
54
55 private final TypeDeclaration<?> declaringClass;
56
57 private final Node locationNode;
58 private final JavaSourceModel sourceModel;
59 private final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
60
61
62 private final JavadocLinkGenerator linkGenerator;
63
64 private final int lineNumber;
65 private final Map<String, Object> attributes = new HashMap<>();
66
67 public JavaClassConverterContext(
68 TypeDeclaration<?> mojoClass,
69 JavaSourceModel sourceModel,
70 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
71 JavadocLinkGenerator linkGenerator,
72 int lineNumber) {
73 this(mojoClass, mojoClass, mojoClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber);
74 }
75
76 public JavaClassConverterContext(
77 TypeDeclaration<?> mojoClass,
78 TypeDeclaration<?> declaringClass,
79 Node locationNode,
80 JavaSourceModel sourceModel,
81 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
82 JavadocLinkGenerator linkGenerator,
83 int lineNumber) {
84 this.mojoClass = mojoClass;
85 this.declaringClass = declaringClass;
86 this.locationNode = locationNode;
87 this.sourceModel = sourceModel;
88 this.mojoAnnotatedClasses = mojoAnnotatedClasses;
89 this.linkGenerator = linkGenerator;
90 this.lineNumber = lineNumber;
91 }
92
93 @Override
94 public Optional<String> getModuleName() {
95 return sourceModel.getModuleName(getPackageName());
96 }
97
98 @Override
99 public String getPackageName() {
100 return resolve(mojoClass).getPackageName();
101 }
102
103 @Override
104 public String getLocation() {
105 return sourceModel.getLocation(locationNode, lineNumber);
106 }
107
108
109 @Override
110 public boolean isReferencedBy(FullyQualifiedJavadocReference reference) {
111 ResolvedReferenceTypeDeclaration declaration = resolve(mojoClass);
112 if (isClassReferencedByReference(declaration, reference)) {
113 return true;
114 }
115 return declaration.getAllAncestors().stream()
116 .map(ResolvedReferenceType::getTypeDeclaration)
117 .filter(Optional::isPresent)
118 .map(Optional::get)
119 .anyMatch(type -> isClassReferencedByReference(type, reference));
120 }
121
122 private static boolean isClassReferencedByReference(
123 ResolvedReferenceTypeDeclaration declaration, FullyQualifiedJavadocReference reference) {
124 return declaration.getPackageName().equals(reference.getPackageName().orElse(""))
125 && declaration.getClassName().equals(reference.getClassName().orElse(""));
126 }
127
128 @Override
129 public boolean canGetUrl() {
130 return linkGenerator != null;
131 }
132
133 @Override
134 public URI getUrl(FullyQualifiedJavadocReference reference) {
135 try {
136 if (isReferencedBy(reference)
137 && MemberType.FIELD == reference.getMemberType().orElse(null)) {
138
139 return new URI(null, null, reference.getMember().orElse(null));
140 }
141 Optional<String> fqClassName = reference.getFullyQualifiedClassName();
142 if (fqClassName.isPresent()) {
143 MojoAnnotatedClass mojoAnnotatedClass = mojoAnnotatedClasses.get(fqClassName.get());
144 if (mojoAnnotatedClass != null
145 && mojoAnnotatedClass.getMojo() != null
146 && (!reference.getLabel().isPresent()
147 || MemberType.FIELD == reference.getMemberType().orElse(null))) {
148
149 return new URI(
150 null,
151 "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
152 reference.getMember().orElse(null));
153 }
154 }
155 } catch (URISyntaxException e) {
156 throw new IllegalStateException("Error constructing a valid URL", e);
157 }
158 if (linkGenerator == null) {
159 throw new IllegalStateException("No Javadoc Sites given to create URLs to");
160 }
161 return linkGenerator.createLink(reference);
162 }
163
164 @Override
165 public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) {
166 Optional<FullyQualifiedJavadocReference> resolved;
167
168 if (reference.getPackageNameClassName().isPresent()) {
169 String name = reference.getPackageNameClassName().get();
170 resolved = resolveNamedReference(name, reference.getMember(), reference.getLabel());
171 if (resolved.isPresent()) {
172 return resolved.get();
173 }
174 }
175
176
177 if (reference.getMember().isPresent()
178 && !reference.getPackageNameClassName().isPresent()) {
179 resolved = resolveMember(resolve(declaringClass), reference.getMember(), reference.getLabel(), true);
180 if (resolved.isPresent()) {
181 return resolved.get();
182 }
183 } else if (reference.getPackageNameClassName().isPresent()) {
184
185 String name = reference.getPackageNameClassName().get();
186 for (String candidate : classNameCandidates(name)) {
187 resolved = resolveNamedReference(candidate, reference.getMember(), reference.getLabel());
188 if (resolved.isPresent()) {
189 return resolved.get();
190 }
191 }
192 }
193 throw new IllegalArgumentException("Could not resolve javadoc reference " + reference);
194 }
195
196 private Optional<FullyQualifiedJavadocReference> resolveNamedReference(
197 String name, Optional<String> member, Optional<String> label) {
198 Optional<ResolvedReferenceTypeDeclaration> type = sourceModel.resolveType(name);
199 if (type.isPresent()) {
200 return resolveMember(type.get(), member, label, true);
201 }
202
203 if (!member.isPresent() && sourceModel.hasPackage(name)) {
204 return Optional.of(new FullyQualifiedJavadocReference(name, label, !sourceModel.isInternalPackage(name)));
205 }
206 return Optional.empty();
207 }
208
209 private List<String> classNameCandidates(String name) {
210 List<String> candidates = new ArrayList<>();
211 ResolvedReferenceTypeDeclaration declaration = resolve(declaringClass);
212
213 candidates.add(declaration.getPackageName() + "." + name);
214 candidates.add("java.lang." + name);
215 List<ImportDeclaration> imports = declaringClass
216 .findCompilationUnit()
217 .<List<ImportDeclaration>>map(unit -> new ArrayList<>(unit.getImports()))
218 .orElseGet(Collections::emptyList);
219 for (ImportDeclaration importDeclaration : imports) {
220 if (importDeclaration.isStatic()) {
221 continue;
222 }
223 String importName = importDeclaration.getNameAsString();
224 if (importDeclaration.isAsterisk()) {
225 candidates.add(importName + "." + name);
226 } else if (name.equals(simpleName(importName))) {
227 candidates.add(importName);
228 } else if (name.startsWith(simpleName(importName) + ".")) {
229
230 candidates.add(
231 importName + name.substring(simpleName(importName).length()));
232 }
233 }
234 return candidates;
235 }
236
237 private Optional<FullyQualifiedJavadocReference> resolveMember(
238 ResolvedReferenceTypeDeclaration type,
239 Optional<String> member,
240 Optional<String> label,
241 boolean includeAncestors) {
242 if (!member.isPresent()) {
243 return Optional.of(toReference(type, Optional.empty(), Optional.empty(), label));
244 }
245
246 String memberText = member.get();
247
248 Optional<ResolvedFieldDeclaration> field = findField(type, memberText, includeAncestors);
249 if (field.isPresent()) {
250 return Optional.of(toReference(
251 field.get().declaringType().asReferenceType(), member, Optional.of(MemberType.FIELD), label));
252 }
253
254 String methodName = methodName(memberText);
255 Optional<List<String>> parameterTypes = parameterTypes(memberText);
256 Optional<ResolvedMethodDeclaration> method = findMethod(type, methodName, parameterTypes, includeAncestors);
257 if (method.isPresent()) {
258 return Optional.of(toReference(
259 method.get().declaringType(),
260 Optional.of(canonicalMember(method.get())),
261 Optional.of(MemberType.METHOD),
262 label));
263 }
264
265 if (methodName.equals(type.getName())) {
266 Optional<ResolvedConstructorDeclaration> constructor = findConstructor(type, parameterTypes);
267 if (constructor.isPresent()) {
268 return Optional.of(toReference(
269 type,
270 Optional.of(canonicalMember(constructor.get())),
271 Optional.of(MemberType.CONSTRUCTOR),
272 label));
273 }
274 }
275 return Optional.empty();
276 }
277
278 private Optional<ResolvedFieldDeclaration> findField(
279 ResolvedReferenceTypeDeclaration type, String name, boolean includeAncestors) {
280 List<ResolvedFieldDeclaration> fields = includeAncestors ? type.getAllFields() : type.getDeclaredFields();
281 return fields.stream().filter(field -> field.getName().equals(name)).findFirst();
282 }
283
284 private Optional<ResolvedMethodDeclaration> findMethod(
285 ResolvedReferenceTypeDeclaration type,
286 String name,
287 Optional<List<String>> parameterTypes,
288 boolean includeAncestors) {
289 List<ResolvedReferenceTypeDeclaration> hierarchy = new ArrayList<>();
290 hierarchy.add(type);
291 if (includeAncestors) {
292 type.getAllAncestors().stream()
293 .map(ResolvedReferenceType::getTypeDeclaration)
294 .filter(Optional::isPresent)
295 .map(Optional::get)
296 .forEach(hierarchy::add);
297 }
298 for (ResolvedReferenceTypeDeclaration declaration : hierarchy) {
299 Optional<ResolvedMethodDeclaration> method = declaration.getDeclaredMethods().stream()
300 .filter(candidate -> candidate.getName().equals(name))
301 .filter(candidate -> matches(candidate, parameterTypes))
302 .findFirst();
303 if (method.isPresent()) {
304 return method;
305 }
306 }
307 return Optional.empty();
308 }
309
310 private Optional<ResolvedConstructorDeclaration> findConstructor(
311 ResolvedReferenceTypeDeclaration type, Optional<List<String>> parameterTypes) {
312 return type.getConstructors().stream()
313 .filter(candidate -> matches(candidate, parameterTypes))
314 .findFirst();
315 }
316
317 private boolean matches(ResolvedMethodLikeDeclaration declaration, Optional<List<String>> parameterTypes) {
318 if (!parameterTypes.isPresent()) {
319 return true;
320 }
321 if (declaration.getNumberOfParams() != parameterTypes.get().size()) {
322 return false;
323 }
324 for (int index = 0; index < declaration.getNumberOfParams(); index++) {
325 String actual = declaration.getParam(index).getType().erasure().describe();
326 if (!actual.equals(parameterTypes.get().get(index))) {
327 return false;
328 }
329 }
330 return true;
331 }
332
333 private Optional<List<String>> parameterTypes(String member) {
334 int opening = member.indexOf('(');
335 int closing = member.lastIndexOf(')');
336 if (opening < 0 && closing < 0) {
337
338 return Optional.empty();
339 }
340 if (opening < 0 || closing < opening) {
341 throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member);
342 }
343 String arguments = member.substring(opening + 1, closing).trim();
344 if (arguments.isEmpty()) {
345 return Optional.of(Collections.emptyList());
346 }
347 List<String> result = new ArrayList<>();
348 for (String argument : splitArguments(arguments)) {
349 result.add(resolveParameterType(stripArgumentName(argument.trim())));
350 }
351 return Optional.of(result);
352 }
353
354 private static List<String> splitArguments(String arguments) {
355 List<String> result = new ArrayList<>();
356 int genericDepth = 0;
357 int start = 0;
358 for (int index = 0; index < arguments.length(); index++) {
359 char ch = arguments.charAt(index);
360 if (ch == '<') {
361 genericDepth++;
362 } else if (ch == '>') {
363 genericDepth--;
364 } else if (ch == ',' && genericDepth == 0) {
365 result.add(arguments.substring(start, index));
366 start = index + 1;
367 }
368 }
369 result.add(arguments.substring(start));
370 return result;
371 }
372
373 private static String stripArgumentName(String argument) {
374 int genericDepth = 0;
375 for (int index = argument.length() - 1; index >= 0; index--) {
376 char ch = argument.charAt(index);
377 if (ch == '>') {
378 genericDepth++;
379 } else if (ch == '<') {
380 genericDepth--;
381 } else if (Character.isWhitespace(ch) && genericDepth == 0) {
382 return argument.substring(0, index).trim();
383 }
384 }
385 return argument;
386 }
387
388 private String resolveParameterType(String typeName) {
389 String normalized = eraseGenerics(typeName.replace("...", "[]"));
390 int dimensions = 0;
391 while (normalized.endsWith("[]")) {
392 dimensions++;
393 normalized = normalized.substring(0, normalized.length() - 2);
394 }
395 String resolved;
396 if (isPrimitive(normalized)) {
397 resolved = normalized;
398 } else {
399 List<String> candidates = new ArrayList<>();
400 candidates.add(normalized);
401 candidates.addAll(classNameCandidates(normalized));
402 resolved = candidates.stream()
403 .filter(candidate -> sourceModel.resolveType(candidate).isPresent())
404 .findFirst()
405 .orElseThrow(
406 () -> new IllegalArgumentException("Found unresolvable method argument type " + typeName));
407 }
408 StringBuilder result = new StringBuilder(resolved);
409 for (int index = 0; index < dimensions; index++) {
410 result.append("[]");
411 }
412 return result.toString();
413 }
414
415 private static String eraseGenerics(String value) {
416 StringBuilder result = new StringBuilder();
417 int depth = 0;
418 for (int index = 0; index < value.length(); index++) {
419 char ch = value.charAt(index);
420 if (ch == '<') {
421 depth++;
422 } else if (ch == '>') {
423 depth--;
424 } else if (depth == 0) {
425 result.append(ch);
426 }
427 }
428 return result.toString();
429 }
430
431 private static boolean isPrimitive(String name) {
432 return "boolean".equals(name)
433 || "byte".equals(name)
434 || "char".equals(name)
435 || "double".equals(name)
436 || "float".equals(name)
437 || "int".equals(name)
438 || "long".equals(name)
439 || "short".equals(name);
440 }
441
442 private FullyQualifiedJavadocReference toReference(
443 ResolvedReferenceTypeDeclaration type,
444 Optional<String> member,
445 Optional<MemberType> memberType,
446 Optional<String> label) {
447 return new FullyQualifiedJavadocReference(
448 type.getPackageName(),
449 Optional.of(type.getClassName()),
450 member,
451 memberType,
452 label,
453 !sourceModel.isInternal(type));
454 }
455
456 @Override
457 public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
458 String fqcn = reference
459 .getFullyQualifiedClassName()
460 .orElseThrow(() ->
461 new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
462 String fieldName = reference
463 .getMember()
464 .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
465 TypeDeclaration<?> type = sourceModel
466 .getType(fqcn)
467 .orElseThrow(() -> new IllegalArgumentException("Could not find source class " + fqcn));
468 for (FieldDeclaration field : type.getFields()) {
469 Optional<com.github.javaparser.ast.body.VariableDeclarator> variable = field.getVariables().stream()
470 .filter(candidate -> candidate.getNameAsString().equals(fieldName))
471 .findFirst();
472 if (variable.isPresent()) {
473 if (!field.isStatic()) {
474 throw new IllegalArgumentException(
475 "Field with name " + fieldName + " in class " + fqcn + " is not static");
476 }
477 return variable.get()
478 .getInitializer()
479 .map(initializer -> initializer.toString())
480 .orElseThrow(() -> new IllegalArgumentException(
481 "Field with name " + fieldName + " in class " + fqcn + " has no initializer"));
482 }
483 }
484 throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
485 }
486
487 @Override
488 public URI getInternalJavadocSiteBaseUrl() {
489 return linkGenerator.getInternalJavadocSiteBaseUrl();
490 }
491
492 private static ResolvedReferenceTypeDeclaration resolve(TypeDeclaration<?> declaration) {
493 return declaration.resolve();
494 }
495
496 private static String methodName(String member) {
497 int opening = member.indexOf('(');
498 return opening < 0 ? member : member.substring(0, opening);
499 }
500
501 private static String canonicalMember(ResolvedMethodLikeDeclaration declaration) {
502 return declaration.getName() + "("
503 + declaration.formalParameterTypes().stream()
504 .map(type -> type.erasure().describe())
505 .collect(Collectors.joining(","))
506 + ")";
507 }
508
509 private static String simpleName(String name) {
510 int separator = name.lastIndexOf('.');
511 return separator < 0 ? name : name.substring(separator + 1);
512 }
513
514 @SuppressWarnings("unchecked")
515 @Override
516 public <T> T setAttribute(String name, T value) {
517 return (T) attributes.put(name, value);
518 }
519
520 @SuppressWarnings("unchecked")
521 @Override
522 public <T> T getAttribute(String name, Class<T> clazz, T defaultValue) {
523 return (T) attributes.getOrDefault(name, defaultValue);
524 }
525 }