001package org.apache.maven.tools.plugin.extractor.annotations.converter; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.net.URL; 025import java.nio.file.Paths; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Optional; 032import java.util.stream.Collectors; 033 034import com.thoughtworks.qdox.JavaProjectBuilder; 035import com.thoughtworks.qdox.builder.TypeAssembler; 036import com.thoughtworks.qdox.library.ClassNameLibrary; 037import com.thoughtworks.qdox.model.JavaClass; 038import com.thoughtworks.qdox.model.JavaField; 039import com.thoughtworks.qdox.model.JavaModule; 040import com.thoughtworks.qdox.model.JavaPackage; 041import com.thoughtworks.qdox.model.JavaType; 042import com.thoughtworks.qdox.parser.structs.TypeDef; 043import com.thoughtworks.qdox.type.TypeResolver; 044import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass; 045import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference; 046import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; 047import org.apache.maven.tools.plugin.javadoc.JavadocReference; 048import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType; 049 050/** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */ 051public class JavaClassConverterContext 052 implements ConverterContext 053{ 054 055 final JavaClass mojoClass; // this is the mojo's class 056 057 final JavaClass declaringClass; // this may be a super class of the mojo's class 058 059 final JavaProjectBuilder javaProjectBuilder; 060 061 final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses; 062 063 final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured 064 065 final int lineNumber; 066 067 final Optional<JavaModule> javaModule; 068 069 final Map<String, Object> attributes; 070 071 public JavaClassConverterContext( JavaClass mojoClass, JavaProjectBuilder javaProjectBuilder, 072 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 073 JavadocLinkGenerator linkGenerator, int lineNumber ) 074 { 075 this( mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber ); 076 } 077 078 public JavaClassConverterContext( JavaClass mojoClass, JavaClass declaringClass, 079 JavaProjectBuilder javaProjectBuilder, 080 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 081 JavadocLinkGenerator linkGenerator, int lineNumber ) 082 { 083 this.mojoClass = mojoClass; 084 this.declaringClass = declaringClass; 085 this.javaProjectBuilder = javaProjectBuilder; 086 this.mojoAnnotatedClasses = mojoAnnotatedClasses; 087 this.linkGenerator = linkGenerator; 088 this.lineNumber = lineNumber; 089 this.attributes = new HashMap<>(); 090 091 javaModule = 092 mojoClass.getJavaClassLibrary().getJavaModules().stream().filter( 093 m -> m.getDescriptor().getExports().stream().anyMatch( 094 e -> e.getSource().getName().equals( getPackageName() ) 095 ) ) 096 .findFirst(); 097 } 098 099 @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}