View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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  /** {@link ConverterContext} backed by JavaParser source declarations and symbol resolution. */
49  public class JavaClassConverterContext implements ConverterContext {
50  
51      /** The Mojo class whose documentation is being generated. */
52      private final TypeDeclaration<?> mojoClass;
53  
54      /** The class declaring the converted Javadoc, possibly a superclass of the Mojo class. */
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      /** The link generator, or {@code null} when no Javadoc site is configured. */
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     /** Returns whether {@code reference} identifies the Mojo class or one of its ancestors. */
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                 // Fields in the current Mojo link to parameter anchors on the same page.
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                     // Fields and whole-class references to another Mojo link to that Mojo's page.
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         // First try the reference exactly as written, which handles fully qualified names.
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         // Search member-only references in the current class or interface first, then its ancestors.
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             // Resolve simple and nested type names using Javadoc's lookup order.
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         // Package references cannot contain members.
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         // Search order: current package, implicit java.lang import, then explicit imports in declaration order.
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                 // An imported outer type may prefix a nested-type reference.
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         // Resolve ambiguous member text as a field, method, then constructor.
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             // Without parentheses, match the first overload found, mirroring javadoc.
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 }