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