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.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Optional; 029import java.util.stream.Collectors; 030 031import com.github.javaparser.ast.ImportDeclaration; 032import com.github.javaparser.ast.Node; 033import com.github.javaparser.ast.body.FieldDeclaration; 034import com.github.javaparser.ast.body.TypeDeclaration; 035import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration; 036import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; 037import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; 038import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration; 039import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; 040import com.github.javaparser.resolution.types.ResolvedReferenceType; 041import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel; 042import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass; 043import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference; 044import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType; 045import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; 046import org.apache.maven.tools.plugin.javadoc.JavadocReference; 047 048/** {@link ConverterContext} backed by JavaParser source declarations and symbol resolution. */ 049public class JavaClassConverterContext implements ConverterContext { 050 051 /** The Mojo class whose documentation is being generated. */ 052 private final TypeDeclaration<?> mojoClass; 053 054 /** The class declaring the converted Javadoc, possibly a superclass of the Mojo class. */ 055 private final TypeDeclaration<?> declaringClass; 056 057 private final Node locationNode; 058 private final JavaSourceModel sourceModel; 059 private final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses; 060 061 /** The link generator, or {@code null} when no Javadoc site is configured. */ 062 private final JavadocLinkGenerator linkGenerator; 063 064 private final int lineNumber; 065 private final Map<String, Object> attributes = new HashMap<>(); 066 067 public JavaClassConverterContext( 068 TypeDeclaration<?> mojoClass, 069 JavaSourceModel sourceModel, 070 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 071 JavadocLinkGenerator linkGenerator, 072 int lineNumber) { 073 this(mojoClass, mojoClass, mojoClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber); 074 } 075 076 public JavaClassConverterContext( 077 TypeDeclaration<?> mojoClass, 078 TypeDeclaration<?> declaringClass, 079 Node locationNode, 080 JavaSourceModel sourceModel, 081 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 082 JavadocLinkGenerator linkGenerator, 083 int lineNumber) { 084 this.mojoClass = mojoClass; 085 this.declaringClass = declaringClass; 086 this.locationNode = locationNode; 087 this.sourceModel = sourceModel; 088 this.mojoAnnotatedClasses = mojoAnnotatedClasses; 089 this.linkGenerator = linkGenerator; 090 this.lineNumber = lineNumber; 091 } 092 093 @Override 094 public Optional<String> getModuleName() { 095 return sourceModel.getModuleName(getPackageName()); 096 } 097 098 @Override 099 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}