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.net.URL;
24  import java.nio.file.Paths;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Optional;
31  import java.util.stream.Collectors;
32  
33  import com.thoughtworks.qdox.JavaProjectBuilder;
34  import com.thoughtworks.qdox.builder.TypeAssembler;
35  import com.thoughtworks.qdox.library.ClassNameLibrary;
36  import com.thoughtworks.qdox.model.JavaClass;
37  import com.thoughtworks.qdox.model.JavaField;
38  import com.thoughtworks.qdox.model.JavaModule;
39  import com.thoughtworks.qdox.model.JavaPackage;
40  import com.thoughtworks.qdox.model.JavaType;
41  import com.thoughtworks.qdox.parser.structs.TypeDef;
42  import com.thoughtworks.qdox.type.TypeResolver;
43  import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
44  import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
45  import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
46  import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
47  import org.apache.maven.tools.plugin.javadoc.JavadocReference;
48  
49  /** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */
50  public class JavaClassConverterContext implements ConverterContext {
51  
52      final JavaClass mojoClass; // this is the mojo's class
53  
54      final JavaClass declaringClass; // this may be a super class of the mojo's class
55  
56      final JavaProjectBuilder javaProjectBuilder;
57  
58      final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
59  
60      final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured
61  
62      final int lineNumber;
63  
64      final Optional<JavaModule> javaModule;
65  
66      final Map<String, Object> attributes;
67  
68      public JavaClassConverterContext(
69              JavaClass mojoClass,
70              JavaProjectBuilder javaProjectBuilder,
71              Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
72              JavadocLinkGenerator linkGenerator,
73              int lineNumber) {
74          this(mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber);
75      }
76  
77      public JavaClassConverterContext(
78              JavaClass mojoClass,
79              JavaClass declaringClass,
80              JavaProjectBuilder javaProjectBuilder,
81              Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
82              JavadocLinkGenerator linkGenerator,
83              int lineNumber) {
84          this.mojoClass = mojoClass;
85          this.declaringClass = declaringClass;
86          this.javaProjectBuilder = javaProjectBuilder;
87          this.mojoAnnotatedClasses = mojoAnnotatedClasses;
88          this.linkGenerator = linkGenerator;
89          this.lineNumber = lineNumber;
90          this.attributes = new HashMap<>();
91  
92          javaModule = mojoClass.getJavaClassLibrary().getJavaModules().stream()
93                  .filter(m -> m.getDescriptor().getExports().stream()
94                          .anyMatch(e -> e.getSource().getName().equals(getPackageName())))
95                  .findFirst();
96      }
97  
98      @Override
99      public Optional<String> getModuleName() {
100         // https://github.com/paul-hammant/qdox/issues/113, module name is not exposed
101         return javaModule.map(JavaModule::getName);
102     }
103 
104     @Override
105     public String getPackageName() {
106         return mojoClass.getPackageName();
107     }
108 
109     @Override
110     public String getLocation() {
111         try {
112             URL url = declaringClass.getSource().getURL();
113             if (url == null) // url is not always available, just emit FQCN in that case
114             {
115                 return declaringClass.getPackageName() + declaringClass.getSimpleName() + ":" + lineNumber;
116             }
117             return Paths.get("").toUri().relativize(url.toURI()) + ":" + lineNumber;
118         } catch (URISyntaxException e) {
119             return declaringClass.getSource().getURL() + ":" + lineNumber;
120         }
121     }
122 
123     /**
124      * @param reference
125      * @return true in case either the current context class or any of its super classes are referenced
126      */
127     @Override
128     public boolean isReferencedBy(FullyQualifiedJavadocReference reference) {
129         JavaClass javaClassInHierarchy = this.mojoClass;
130         while (javaClassInHierarchy != null) {
131             if (isClassReferencedByReference(javaClassInHierarchy, reference)) {
132                 return true;
133             }
134             // check implemented interfaces
135             for (JavaClass implementedInterfaces : javaClassInHierarchy.getInterfaces()) {
136                 if (isClassReferencedByReference(implementedInterfaces, reference)) {
137                     return true;
138                 }
139             }
140             javaClassInHierarchy = javaClassInHierarchy.getSuperJavaClass();
141         }
142         return false;
143     }
144 
145     private static boolean isClassReferencedByReference(JavaClass javaClass, FullyQualifiedJavadocReference reference) {
146         return javaClass.getPackageName().equals(reference.getPackageName().orElse(""))
147                 && javaClass.getSimpleName().equals(reference.getClassName().orElse(""));
148     }
149 
150     @Override
151     public boolean canGetUrl() {
152         return linkGenerator != null;
153     }
154 
155     @Override
156     public URI getUrl(FullyQualifiedJavadocReference reference) {
157         try {
158             if (isReferencedBy(reference)
159                     && MemberType.FIELD == reference.getMemberType().orElse(null)) {
160                 // link to current goal's parameters
161                 return new URI(null, null, reference.getMember().orElse(null)); // just an anchor if same context
162             }
163             Optional<String> fqClassName = reference.getFullyQualifiedClassName();
164             if (fqClassName.isPresent()) {
165                 MojoAnnotatedClass mojoAnnotatedClass = mojoAnnotatedClasses.get(fqClassName.get());
166                 if (mojoAnnotatedClass != null
167                         && mojoAnnotatedClass.getMojo() != null
168                         && (!reference.getLabel().isPresent()
169                                 || MemberType.FIELD == reference.getMemberType().orElse(null))) {
170                     // link to other mojo (only for fields = parameters or without member)
171                     return new URI(
172                             null,
173                             "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
174                             reference.getMember().orElse(null));
175                 }
176             }
177         } catch (URISyntaxException e) {
178             throw new IllegalStateException("Error constructing a valid URL", e); // should not happen
179         }
180         if (linkGenerator == null) {
181             throw new IllegalStateException("No Javadoc Sites given to create URLs to");
182         }
183         return linkGenerator.createLink(reference);
184     }
185 
186     @Override
187     public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) {
188         Optional<FullyQualifiedJavadocReference> resolvedName;
189         // is it already fully qualified?
190         if (reference.getPackageNameClassName().isPresent()) {
191             resolvedName = resolveMember(
192                     reference.getPackageNameClassName().get(), reference.getMember(), reference.getLabel());
193             if (resolvedName.isPresent()) {
194                 return resolvedName.get();
195             }
196         }
197         // is it a member only?
198         if (reference.getMember().isPresent()
199                 && !reference.getPackageNameClassName().isPresent()) {
200             // search order for not fully qualified names:
201             // 1. The current class or interface (only for members)
202             resolvedName = resolveMember(declaringClass, reference.getMember(), reference.getLabel());
203             if (resolvedName.isPresent()) {
204                 return resolvedName.get();
205             }
206             // 2. Any enclosing classes and interfaces searching the closest first (only members)
207             for (JavaClass nestedClass : declaringClass.getNestedClasses()) {
208                 resolvedName = resolveMember(nestedClass, reference.getMember(), reference.getLabel());
209                 if (resolvedName.isPresent()) {
210                     return resolvedName.get();
211                 }
212             }
213             // 3. Any superclasses and superinterfaces, searching the closest first. (only members)
214             JavaClass superClass = declaringClass.getSuperJavaClass();
215             while (superClass != null) {
216                 resolvedName = resolveMember(superClass, reference.getMember(), reference.getLabel());
217                 if (resolvedName.isPresent()) {
218                     return resolvedName.get();
219                 }
220                 superClass = superClass.getSuperJavaClass();
221             }
222         } else {
223             String packageNameClassName = reference.getPackageNameClassName().get();
224             // 4. The current package
225             resolvedName = resolveMember(
226                     declaringClass.getPackageName() + "." + packageNameClassName,
227                     reference.getMember(),
228                     reference.getLabel());
229             if (resolvedName.isPresent()) {
230                 return resolvedName.get();
231             }
232             // 5. Any imported packages, classes, and interfaces, searching in the order of the import statement.
233             List<String> importNames = new ArrayList<>();
234             importNames.add("java.lang.*"); // default import
235             importNames.addAll(declaringClass.getSource().getImports());
236             for (String importName : importNames) {
237                 if (importName.endsWith(".*")) {
238                     resolvedName = resolveMember(
239                             importName.replace("*", packageNameClassName), reference.getMember(), reference.getLabel());
240                     if (resolvedName.isPresent()) {
241                         return resolvedName.get();
242                     }
243                 } else {
244                     if (importName.endsWith(packageNameClassName)) {
245                         resolvedName = resolveMember(importName, reference.getMember(), reference.getLabel());
246                         if (resolvedName.isPresent()) {
247                             return resolvedName.get();
248                         }
249                     } else {
250                         // ends with prefix of reference (nested class name)
251                         int firstDotIndex = packageNameClassName.indexOf(".");
252                         if (firstDotIndex > 0
253                                 && importName.endsWith(packageNameClassName.substring(0, firstDotIndex))) {
254                             resolvedName = resolveMember(
255                                     importName,
256                                     packageNameClassName.substring(firstDotIndex + 1),
257                                     reference.getMember(),
258                                     reference.getLabel());
259                             if (resolvedName.isPresent()) {
260                                 return resolvedName.get();
261                             }
262                         }
263                     }
264                 }
265             }
266         }
267         throw new IllegalArgumentException("Could not resolve javadoc reference " + reference);
268     }
269 
270     @Override
271     public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
272         String fqcn = reference
273                 .getFullyQualifiedClassName()
274                 .orElseThrow(() ->
275                         new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
276         String fieldName = reference
277                 .getMember()
278                 .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
279         JavaClass javaClass = javaProjectBuilder.getClassByName(fqcn);
280         JavaField javaField = javaClass.getFieldByName(fieldName);
281         if (javaField == null) {
282             throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
283         }
284         if (!javaField.isStatic()) {
285             throw new IllegalArgumentException("Field with name " + fieldName + " in class " + fqcn + " is not static");
286         }
287         return javaField.getInitializationExpression();
288     }
289 
290     @Override
291     public URI getInternalJavadocSiteBaseUrl() {
292         return linkGenerator.getInternalJavadocSiteBaseUrl();
293     }
294 
295     private Optional<FullyQualifiedJavadocReference> resolveMember(
296             String fullyQualifiedPackageNameClassName, Optional<String> member, Optional<String> label) {
297         return resolveMember(fullyQualifiedPackageNameClassName, "", member, label);
298     }
299 
300     private Optional<FullyQualifiedJavadocReference> resolveMember(
301             String fullyQualifiedPackageNameClassName,
302             String nestedClassName,
303             Optional<String> member,
304             Optional<String> label) {
305         JavaClass javaClass = javaProjectBuilder.getClassByName(fullyQualifiedPackageNameClassName);
306         if (!isClassFound(javaClass)) {
307             JavaPackage javaPackage = javaProjectBuilder.getPackageByName(fullyQualifiedPackageNameClassName);
308             if (javaPackage == null || !nestedClassName.isEmpty()) {
309                 // is it a nested class?
310                 int lastIndexOfDot = fullyQualifiedPackageNameClassName.lastIndexOf('.');
311                 if (lastIndexOfDot > 0) {
312                     String newNestedClassName = nestedClassName;
313                     if (!newNestedClassName.isEmpty()) {
314                         newNestedClassName += '.';
315                     }
316                     newNestedClassName += fullyQualifiedPackageNameClassName.substring(lastIndexOfDot + 1);
317                     return resolveMember(
318                             fullyQualifiedPackageNameClassName.substring(0, lastIndexOfDot),
319                             newNestedClassName,
320                             member,
321                             label);
322                 }
323                 return Optional.empty();
324             } else {
325                 // reference to java package never has a member
326                 return Optional.of(
327                         new FullyQualifiedJavadocReference(javaPackage.getName(), label, isExternal(javaPackage)));
328             }
329         } else {
330             if (!nestedClassName.isEmpty()) {
331                 javaClass = javaClass.getNestedClassByName(nestedClassName);
332                 if (javaClass == null) {
333                     return Optional.empty();
334                 }
335             }
336 
337             return resolveMember(javaClass, member, label);
338         }
339     }
340 
341     private boolean isExternal(JavaClass javaClass) {
342         return isExternal(javaClass.getPackage());
343     }
344 
345     private boolean isExternal(JavaPackage javaPackage) {
346         return !javaPackage.getJavaClassLibrary().equals(mojoClass.getJavaClassLibrary());
347     }
348 
349     private Optional<FullyQualifiedJavadocReference> resolveMember(
350             JavaClass javaClass, Optional<String> member, Optional<String> label) {
351         final Optional<MemberType> memberType;
352         Optional<String> resolvedMember = member;
353         if (member.isPresent()) {
354             // member is either field...
355             if (javaClass.getFieldByName(member.get()) == null) {
356                 // ...is method...
357                 List<JavaType> parameterTypes = getParameterTypes(member.get());
358                 String methodName = getMethodName(member.get());
359                 if (javaClass.getMethodBySignature(methodName, parameterTypes) == null) {
360                     // ...or is constructor
361                     if ((!methodName.equals(javaClass.getSimpleName()))
362                             || (javaClass.getConstructor(parameterTypes) == null)) {
363                         return Optional.empty();
364                     } else {
365                         memberType = Optional.of(MemberType.CONSTRUCTOR);
366                     }
367                 } else {
368                     memberType = Optional.of(MemberType.METHOD);
369                 }
370                 // reconstruct member with fully qualified names but leaving out the argument names
371                 StringBuilder memberBuilder = new StringBuilder(methodName);
372                 memberBuilder.append("(");
373                 memberBuilder.append(parameterTypes.stream()
374                         .map(JavaType::getFullyQualifiedName)
375                         .collect(Collectors.joining(",")));
376                 memberBuilder.append(")");
377                 resolvedMember = Optional.of(memberBuilder.toString());
378             } else {
379                 memberType = Optional.of(MemberType.FIELD);
380             }
381         } else {
382             memberType = Optional.empty();
383         }
384         String className = javaClass
385                 .getCanonicalName()
386                 .substring(javaClass.getPackageName().length() + 1);
387         return Optional.of(new FullyQualifiedJavadocReference(
388                 javaClass.getPackageName(),
389                 Optional.of(className),
390                 resolvedMember,
391                 memberType,
392                 label,
393                 isExternal(javaClass)));
394     }
395 
396     private static boolean isClassFound(JavaClass javaClass) {
397         // this is never null due to using the ClassNameLibrary in the builder
398         // but every instance of ClassNameLibrary basically means that the class was not found
399         return !(javaClass.getJavaClassLibrary() instanceof ClassNameLibrary);
400     }
401 
402     // https://github.com/paul-hammant/qdox/issues/104
403     private List<JavaType> getParameterTypes(String member) {
404         List<JavaType> parameterTypes = new ArrayList<>();
405         // TypeResolver.byClassName() always resolves types as non existing inner class
406         TypeResolver typeResolver = TypeResolver.byClassName(
407                 declaringClass.getPackageName(),
408                 declaringClass.getJavaClassLibrary(),
409                 declaringClass.getSource().getImports());
410 
411         // method parameters are optionally enclosed by parentheses
412         int indexOfOpeningParenthesis = member.indexOf('(');
413         int indexOfClosingParenthesis = member.indexOf(')');
414         final String signatureArguments;
415         if (indexOfOpeningParenthesis >= 0
416                 && indexOfClosingParenthesis > 0
417                 && indexOfClosingParenthesis > indexOfOpeningParenthesis) {
418             signatureArguments = member.substring(indexOfOpeningParenthesis + 1, indexOfClosingParenthesis);
419         } else if (indexOfOpeningParenthesis == -1 && indexOfClosingParenthesis >= 0
420                 || indexOfOpeningParenthesis >= 0 && indexOfOpeningParenthesis == -1) {
421             throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member);
422         } else {
423             // If any method or constructor is entered as a name with no parentheses, such as getValue,
424             // and if there is no field with the same name, then the javadoc command still creates a
425             // link to the method. If this method is overloaded, then the javadoc command links to the
426             // first method its search encounters, which is unspecified
427             // (Source: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#JSWOR654)
428             return Collections.emptyList();
429         }
430         for (String parameter : signatureArguments.split(",")) {
431             // strip off argument name, only type is relevant
432             String canonicalParameter = parameter.trim();
433             int spaceIndex = canonicalParameter.indexOf(' ');
434             final String typeName;
435             if (spaceIndex > 0) {
436                 typeName = canonicalParameter.substring(0, spaceIndex).trim();
437             } else {
438                 typeName = canonicalParameter;
439             }
440             if (!typeName.isEmpty()) {
441                 String rawTypeName = getRawTypeName(typeName);
442                 // already check here for unresolvable types due to https://github.com/paul-hammant/qdox/issues/111
443                 if (typeResolver.resolveType(rawTypeName) == null) {
444                     throw new IllegalArgumentException("Found unresolvable method argument type in " + member);
445                 }
446                 TypeDef typeDef = new TypeDef(getRawTypeName(typeName));
447                 int dimensions = getDimensions(typeName);
448                 JavaType javaType = TypeAssembler.createUnresolved(typeDef, dimensions, typeResolver);
449 
450                 parameterTypes.add(javaType);
451             }
452         }
453         return parameterTypes;
454     }
455 
456     private static int getDimensions(String type) {
457         return (int) type.chars().filter(ch -> ch == '[').count();
458     }
459 
460     private static String getRawTypeName(String typeName) {
461         // strip dimensions
462         int indexOfOpeningBracket = typeName.indexOf('[');
463         if (indexOfOpeningBracket >= 0) {
464             return typeName.substring(0, indexOfOpeningBracket);
465         } else {
466             return typeName;
467         }
468     }
469 
470     private static String getMethodName(String member) {
471         // name is separated from arguments either by '(' or spans the full member
472         int indexOfOpeningParentheses = member.indexOf('(');
473         if (indexOfOpeningParentheses == -1) {
474             return member;
475         } else {
476             return member.substring(0, indexOfOpeningParentheses);
477         }
478     }
479 
480     @SuppressWarnings("unchecked")
481     @Override
482     public <T> T setAttribute(String name, T value) {
483         return (T) attributes.put(name, value);
484     }
485 
486     @SuppressWarnings("unchecked")
487     @Override
488     public <T> T getAttribute(String name, Class<T> clazz, T defaultValue) {
489         return (T) attributes.getOrDefault(name, defaultValue);
490     }
491 }