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