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; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023import javax.inject.Singleton; 024 025import java.io.File; 026import java.net.MalformedURLException; 027import java.net.URL; 028import java.net.URLClassLoader; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Comparator; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.List; 037import java.util.Map; 038import java.util.Objects; 039import java.util.Optional; 040import java.util.Set; 041import java.util.TreeMap; 042import java.util.TreeSet; 043import java.util.stream.Collectors; 044 045import com.thoughtworks.qdox.JavaProjectBuilder; 046import com.thoughtworks.qdox.library.SortedClassLibraryBuilder; 047import com.thoughtworks.qdox.model.DocletTag; 048import com.thoughtworks.qdox.model.JavaAnnotatedElement; 049import com.thoughtworks.qdox.model.JavaClass; 050import com.thoughtworks.qdox.model.JavaField; 051import com.thoughtworks.qdox.model.JavaMember; 052import com.thoughtworks.qdox.model.JavaMethod; 053import org.apache.maven.artifact.Artifact; 054import org.apache.maven.artifact.versioning.ComparableVersion; 055import org.apache.maven.plugin.descriptor.InvalidParameterException; 056import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException; 057import org.apache.maven.plugin.descriptor.MojoDescriptor; 058import org.apache.maven.plugin.descriptor.PluginDescriptor; 059import org.apache.maven.plugin.descriptor.Requirement; 060import org.apache.maven.project.MavenProject; 061import org.apache.maven.tools.plugin.ExtendedMojoDescriptor; 062import org.apache.maven.tools.plugin.PluginToolsRequest; 063import org.apache.maven.tools.plugin.extractor.ExtractionException; 064import org.apache.maven.tools.plugin.extractor.GroupKey; 065import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor; 066import org.apache.maven.tools.plugin.extractor.annotations.converter.ConverterContext; 067import org.apache.maven.tools.plugin.extractor.annotations.converter.JavaClassConverterContext; 068import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter; 069import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter; 070import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent; 071import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent; 072import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent; 073import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent; 074import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass; 075import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScanner; 076import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScannerRequest; 077import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; 078import org.codehaus.plexus.archiver.ArchiverException; 079import org.codehaus.plexus.archiver.UnArchiver; 080import org.codehaus.plexus.archiver.manager.ArchiverManager; 081import org.codehaus.plexus.archiver.manager.NoSuchArchiverException; 082import org.codehaus.plexus.logging.AbstractLogEnabled; 083import org.codehaus.plexus.util.StringUtils; 084import org.eclipse.aether.RepositorySystem; 085import org.eclipse.aether.artifact.DefaultArtifact; 086import org.eclipse.aether.resolution.ArtifactRequest; 087import org.eclipse.aether.resolution.ArtifactResolutionException; 088import org.eclipse.aether.resolution.ArtifactResult; 089import org.objectweb.asm.Opcodes; 090 091/** 092 * JavaMojoDescriptorExtractor, a MojoDescriptor extractor to read descriptors from java classes with annotations. 093 * Notice that source files are also parsed to get description, since and deprecation information. 094 * 095 * @author Olivier Lamy 096 * @since 3.0 097 */ 098@Named(JavaAnnotationsMojoDescriptorExtractor.NAME) 099@Singleton 100public class JavaAnnotationsMojoDescriptorExtractor extends AbstractLogEnabled implements MojoDescriptorExtractor { 101 public static final String NAME = "java-annotations"; 102 103 private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 100); 104 105 /** 106 * 107 * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a> 108 */ 109 private static final Map<Integer, String> CLASS_VERSION_TO_JAVA_STRING; 110 111 static { 112 CLASS_VERSION_TO_JAVA_STRING = new HashMap<>(); 113 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_1, "1.1"); 114 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_2, "1.2"); 115 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_3, "1.3"); 116 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_4, "1.4"); 117 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_5, "1.5"); 118 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_6, "1.6"); 119 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_7, "1.7"); 120 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_8, "1.8"); 121 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V9, "9"); 122 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V10, "10"); 123 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V11, "11"); 124 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V12, "12"); 125 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V13, "13"); 126 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V14, "14"); 127 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V15, "15"); 128 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V16, "16"); 129 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V17, "17"); 130 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V18, "18"); 131 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V19, "19"); 132 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V20, "20"); 133 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V21, "21"); 134 CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V22, "22"); 135 } 136 137 @Inject 138 MojoAnnotationsScanner mojoAnnotationsScanner; 139 140 @Inject 141 private RepositorySystem repositorySystem; 142 143 @Inject 144 private ArchiverManager archiverManager; 145 146 @Inject 147 private JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter; 148 149 @Inject 150 private JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter; 151 152 @Override 153 public String getName() { 154 return NAME; 155 } 156 157 @Override 158 public boolean isDeprecated() { 159 return false; // this is the "current way" to write Java Mojos 160 } 161 162 @Override 163 public GroupKey getGroupKey() { 164 return GROUP_KEY; 165 } 166 167 /** 168 * Compares class file format versions. 169 * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a> 170 * 171 */ 172 @SuppressWarnings("checkstyle:magicnumber") 173 static final class ClassVersionComparator implements Comparator<Integer> { 174 @Override 175 public int compare(Integer classVersion1, Integer classVersion2) { 176 // first compare major version ( 177 int result = Integer.compare(classVersion1 & 0x00FF, classVersion2 & 0x00FF); 178 if (result == 0) { 179 // compare minor version if major is equal 180 result = Integer.compare(classVersion1, classVersion2); 181 } 182 return result; 183 } 184 } 185 186 @Override 187 public List<MojoDescriptor> execute(PluginToolsRequest request) 188 throws ExtractionException, InvalidPluginDescriptorException { 189 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = scanAnnotations(request); 190 191 Optional<Integer> maxClassVersion = mojoAnnotatedClasses.values().stream() 192 .map(MojoAnnotatedClass::getClassVersion) 193 .max(new ClassVersionComparator()); 194 if (maxClassVersion.isPresent()) { 195 String requiredJavaVersion = CLASS_VERSION_TO_JAVA_STRING.get(maxClassVersion.get()); 196 if (StringUtils.isBlank(request.getRequiredJavaVersion()) 197 || new ComparableVersion(request.getRequiredJavaVersion()) 198 .compareTo(new ComparableVersion(requiredJavaVersion)) 199 < 0) { 200 request.setRequiredJavaVersion(requiredJavaVersion); 201 } 202 } 203 JavaProjectBuilder builder = scanJavadoc(request, mojoAnnotatedClasses.values()); 204 Map<String, JavaClass> javaClassesMap = discoverClasses(builder); 205 206 final JavadocLinkGenerator linkGenerator; 207 if (request.getInternalJavadocBaseUrl() != null 208 || (request.getExternalJavadocBaseUrls() != null 209 && !request.getExternalJavadocBaseUrls().isEmpty())) { 210 linkGenerator = new JavadocLinkGenerator( 211 request.getInternalJavadocBaseUrl(), 212 request.getInternalJavadocVersion(), 213 request.getExternalJavadocBaseUrls(), 214 request.getSettings()); 215 } else { 216 linkGenerator = null; 217 } 218 219 populateDataFromJavadoc(builder, mojoAnnotatedClasses, javaClassesMap, linkGenerator); 220 221 return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor()); 222 } 223 224 private Map<String, MojoAnnotatedClass> scanAnnotations(PluginToolsRequest request) throws ExtractionException { 225 MojoAnnotationsScannerRequest mojoAnnotationsScannerRequest = new MojoAnnotationsScannerRequest(); 226 227 File output = new File(request.getProject().getBuild().getOutputDirectory()); 228 mojoAnnotationsScannerRequest.setClassesDirectories(Arrays.asList(output)); 229 230 mojoAnnotationsScannerRequest.setDependencies(request.getDependencies()); 231 232 mojoAnnotationsScannerRequest.setProject(request.getProject()); 233 234 Map<String, MojoAnnotatedClass> result = mojoAnnotationsScanner.scan(mojoAnnotationsScannerRequest); 235 request.setUsedMavenApiVersion(mojoAnnotationsScannerRequest.getMavenApiVersion()); 236 return result; 237 } 238 239 private JavaProjectBuilder scanJavadoc( 240 PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses) 241 throws ExtractionException { 242 // found artifact from reactors to scan sources 243 // we currently only scan sources from reactors 244 List<MavenProject> mavenProjects = new ArrayList<>(); 245 246 // if we need to scan sources from external artifacts 247 Set<Artifact> externalArtifacts = new HashSet<>(); 248 249 JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder()); 250 builder.setEncoding(request.getEncoding()); 251 extendJavaProjectBuilder(builder, request.getProject()); 252 253 for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) { 254 if (Objects.equals( 255 mojoAnnotatedClass.getArtifact().getArtifactId(), 256 request.getProject().getArtifact().getArtifactId())) { 257 continue; 258 } 259 260 if (!isMojoAnnnotatedClassCandidate(mojoAnnotatedClass)) { 261 // we don't scan sources for classes without mojo annotations 262 continue; 263 } 264 265 MavenProject mavenProject = 266 getFromProjectReferences(mojoAnnotatedClass.getArtifact(), request.getProject()); 267 268 if (mavenProject != null) { 269 mavenProjects.add(mavenProject); 270 } else { 271 externalArtifacts.add(mojoAnnotatedClass.getArtifact()); 272 } 273 } 274 275 // try to get artifact with sources classifier, extract somewhere then scan for @since, @deprecated 276 for (Artifact artifact : externalArtifacts) { 277 // parameter for test-sources too ?? olamy I need that for it test only 278 if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) { 279 extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "test-sources"); 280 } else { 281 extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "sources"); 282 } 283 } 284 285 for (MavenProject mavenProject : mavenProjects) { 286 extendJavaProjectBuilder(builder, mavenProject); 287 } 288 289 return builder; 290 } 291 292 private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) { 293 return mojoAnnotatedClass != null && mojoAnnotatedClass.hasAnnotations(); 294 } 295 296 /** 297 * from sources scan to get @since and @deprecated and description of classes and fields. 298 */ 299 protected void populateDataFromJavadoc( 300 JavaProjectBuilder javaProjectBuilder, 301 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 302 Map<String, JavaClass> javaClassesMap, 303 JavadocLinkGenerator linkGenerator) { 304 305 for (Map.Entry<String, MojoAnnotatedClass> entry : mojoAnnotatedClasses.entrySet()) { 306 JavaClass javaClass = javaClassesMap.get(entry.getKey()); 307 if (javaClass == null) { 308 continue; 309 } 310 // populate class-level content 311 MojoAnnotationContent mojoAnnotationContent = entry.getValue().getMojo(); 312 if (mojoAnnotationContent != null) { 313 JavaClassConverterContext context = new JavaClassConverterContext( 314 javaClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, javaClass.getLineNumber()); 315 mojoAnnotationContent.setDescription(getDescriptionFromElement(javaClass, context)); 316 317 DocletTag since = findInClassHierarchy(javaClass, "since"); 318 if (since != null) { 319 mojoAnnotationContent.setSince(getRawValueFromTaglet(since, context)); 320 } 321 322 DocletTag deprecated = findInClassHierarchy(javaClass, "deprecated"); 323 if (deprecated != null) { 324 mojoAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context)); 325 } 326 } 327 328 Map<String, JavaAnnotatedElement> fieldsMap = extractFieldsAnnotations(javaClass, javaClassesMap); 329 Map<String, JavaAnnotatedElement> methodsMap = extractMethodsAnnotations(javaClass, javaClassesMap); 330 331 // populate parameters 332 Map<String, ParameterAnnotationContent> parameters = 333 getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses); 334 parameters = new TreeMap<>(parameters); 335 for (Map.Entry<String, ParameterAnnotationContent> parameter : parameters.entrySet()) { 336 JavaAnnotatedElement element; 337 if (parameter.getValue().isAnnotationOnMethod()) { 338 element = methodsMap.get(parameter.getKey()); 339 } else { 340 element = fieldsMap.get(parameter.getKey()); 341 } 342 343 if (element == null) { 344 continue; 345 } 346 347 JavaClassConverterContext context = new JavaClassConverterContext( 348 javaClass, ((JavaMember) element).getDeclaringClass(), 349 javaProjectBuilder, mojoAnnotatedClasses, 350 linkGenerator, element.getLineNumber()); 351 ParameterAnnotationContent parameterAnnotationContent = parameter.getValue(); 352 parameterAnnotationContent.setDescription(getDescriptionFromElement(element, context)); 353 354 DocletTag deprecated = element.getTagByName("deprecated"); 355 if (deprecated != null) { 356 parameterAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context)); 357 } 358 359 DocletTag since = element.getTagByName("since"); 360 if (since != null) { 361 parameterAnnotationContent.setSince(getRawValueFromTaglet(since, context)); 362 } 363 } 364 365 // populate components 366 Map<String, ComponentAnnotationContent> components = 367 entry.getValue().getComponents(); 368 for (Map.Entry<String, ComponentAnnotationContent> component : components.entrySet()) { 369 JavaAnnotatedElement element = fieldsMap.get(component.getKey()); 370 if (element == null) { 371 continue; 372 } 373 374 JavaClassConverterContext context = new JavaClassConverterContext( 375 javaClass, ((JavaMember) element).getDeclaringClass(), 376 javaProjectBuilder, mojoAnnotatedClasses, 377 linkGenerator, javaClass.getLineNumber()); 378 ComponentAnnotationContent componentAnnotationContent = component.getValue(); 379 componentAnnotationContent.setDescription(getDescriptionFromElement(element, context)); 380 381 DocletTag deprecated = element.getTagByName("deprecated"); 382 if (deprecated != null) { 383 componentAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context)); 384 } 385 386 DocletTag since = element.getTagByName("since"); 387 if (since != null) { 388 componentAnnotationContent.setSince(getRawValueFromTaglet(since, context)); 389 } 390 } 391 } 392 } 393 394 /** 395 * Returns the XHTML description from the given element. 396 * This may refer to either goal, parameter or component. 397 * @param element the element for which to generate the description 398 * @param context the context with which to call the converter 399 * @return the generated description 400 */ 401 String getDescriptionFromElement(JavaAnnotatedElement element, JavaClassConverterContext context) { 402 403 String comment = element.getComment(); 404 if (comment == null) { 405 return null; 406 } 407 StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(comment, context)); 408 for (DocletTag docletTag : element.getTags()) { 409 // also consider see block tags 410 if ("see".equals(docletTag.getName())) { 411 description.append(javadocBlockTagsToHtmlConverter.convert(docletTag, context)); 412 } 413 } 414 return description.toString(); 415 } 416 417 String getRawValueFromTaglet(DocletTag docletTag, ConverterContext context) { 418 // just resolve inline tags and convert to XHTML 419 return javadocInlineTagsToHtmlConverter.convert(docletTag.getValue(), context); 420 } 421 422 /** 423 * @param javaClass not null 424 * @param tagName not null 425 * @return docletTag instance 426 */ 427 private DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) { 428 try { 429 DocletTag tag = javaClass.getTagByName(tagName); 430 431 if (tag == null) { 432 JavaClass superClass = javaClass.getSuperJavaClass(); 433 434 if (superClass != null) { 435 tag = findInClassHierarchy(superClass, tagName); 436 } 437 } 438 439 return tag; 440 } catch (NoClassDefFoundError e) { 441 if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) { 442 return null; 443 } 444 String str; 445 try { 446 str = javaClass.getFullyQualifiedName(); 447 } catch (Throwable t) { 448 str = javaClass.getValue(); 449 } 450 getLogger().warn("Failed extracting tag '" + tagName + "' from class " + str); 451 throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e); 452 } 453 } 454 455 /** 456 * extract fields that are either parameters or components. 457 * 458 * @param javaClass not null 459 * @return map with Mojo parameters names as keys 460 */ 461 private Map<String, JavaAnnotatedElement> extractFieldsAnnotations( 462 JavaClass javaClass, Map<String, JavaClass> javaClassesMap) { 463 try { 464 Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>(); 465 466 // we have to add the parent fields first, so that they will be overwritten by the local fields if 467 // that actually happens... 468 JavaClass superClass = javaClass.getSuperJavaClass(); 469 470 if (superClass != null) { 471 if (!superClass.getFields().isEmpty()) { 472 rawParams = extractFieldsAnnotations(superClass, javaClassesMap); 473 } 474 // maybe sources comes from scan of sources artifact 475 superClass = javaClassesMap.get(superClass.getFullyQualifiedName()); 476 if (superClass != null && !superClass.getFields().isEmpty()) { 477 rawParams = extractFieldsAnnotations(superClass, javaClassesMap); 478 } 479 } else { 480 481 rawParams = new TreeMap<>(); 482 } 483 484 for (JavaField field : javaClass.getFields()) { 485 rawParams.put(field.getName(), field); 486 } 487 488 return rawParams; 489 } catch (NoClassDefFoundError e) { 490 getLogger().warn("Failed extracting parameters from " + javaClass); 491 throw e; 492 } 493 } 494 495 /** 496 * extract methods that are parameters. 497 * 498 * @param javaClass not null 499 * @return map with Mojo parameters names as keys 500 */ 501 private Map<String, JavaAnnotatedElement> extractMethodsAnnotations( 502 JavaClass javaClass, Map<String, JavaClass> javaClassesMap) { 503 try { 504 Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>(); 505 506 // we have to add the parent methods first, so that they will be overwritten by the local methods if 507 // that actually happens... 508 JavaClass superClass = javaClass.getSuperJavaClass(); 509 510 if (superClass != null) { 511 if (!superClass.getMethods().isEmpty()) { 512 rawParams = extractMethodsAnnotations(superClass, javaClassesMap); 513 } 514 // maybe sources comes from scan of sources artifact 515 superClass = javaClassesMap.get(superClass.getFullyQualifiedName()); 516 if (superClass != null && !superClass.getMethods().isEmpty()) { 517 rawParams = extractMethodsAnnotations(superClass, javaClassesMap); 518 } 519 } else { 520 521 rawParams = new TreeMap<>(); 522 } 523 524 for (JavaMethod method : javaClass.getMethods()) { 525 if (isPublicSetterMethod(method)) { 526 rawParams.put( 527 StringUtils.lowercaseFirstLetter(method.getName().substring(3)), method); 528 } 529 } 530 531 return rawParams; 532 } catch (NoClassDefFoundError e) { 533 if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) { 534 return new TreeMap<>(); 535 } 536 String str; 537 try { 538 str = javaClass.getFullyQualifiedName(); 539 } catch (Throwable t) { 540 str = javaClass.getValue(); 541 } 542 getLogger().warn("Failed extracting methods from " + str); 543 throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e); 544 } 545 } 546 547 private boolean isPublicSetterMethod(JavaMethod method) { 548 return method.isPublic() 549 && !method.isStatic() 550 && method.getName().length() > 3 551 && (method.getName().startsWith("add") || method.getName().startsWith("set")) 552 && "void".equals(method.getReturnType().getValue()) 553 && method.getParameters().size() == 1; 554 } 555 556 protected Map<String, JavaClass> discoverClasses(JavaProjectBuilder builder) { 557 Collection<JavaClass> javaClasses = builder.getClasses(); 558 559 if (javaClasses == null || javaClasses.size() < 1) { 560 return Collections.emptyMap(); 561 } 562 563 Map<String, JavaClass> javaClassMap = new HashMap<>(javaClasses.size()); 564 565 for (JavaClass javaClass : javaClasses) { 566 javaClassMap.put(javaClass.getFullyQualifiedName(), javaClass); 567 } 568 569 return javaClassMap; 570 } 571 572 protected void extendJavaProjectBuilderWithSourcesJar( 573 JavaProjectBuilder builder, Artifact artifact, PluginToolsRequest request, String classifier) 574 throws ExtractionException { 575 try { 576 org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact( 577 artifact.getGroupId(), 578 artifact.getArtifactId(), 579 classifier, 580 artifact.getArtifactHandler().getExtension(), 581 artifact.getVersion()); 582 583 ArtifactRequest resolveRequest = 584 new ArtifactRequest(sourcesArtifact, request.getProject().getRemoteProjectRepositories(), null); 585 try { 586 ArtifactResult result = repositorySystem.resolveArtifact(request.getRepoSession(), resolveRequest); 587 sourcesArtifact = result.getArtifact(); 588 } catch (ArtifactResolutionException e) { 589 String message = "Unable to get sources artifact for " + artifact.getId() 590 + ". Some javadoc tags (@since, @deprecated and comments) won't be used"; 591 if (getLogger().isDebugEnabled()) { 592 getLogger().warn(message, e); 593 } else { 594 getLogger().warn(message); 595 } 596 return; 597 } 598 599 if (sourcesArtifact.getFile() == null || !sourcesArtifact.getFile().exists()) { 600 // could not get artifact sources 601 return; 602 } 603 604 if (sourcesArtifact.getFile().isFile()) { 605 // extract sources to target/maven-plugin-plugin-sources/${groupId}/${artifact}/sources 606 File extractDirectory = new File( 607 request.getProject().getBuild().getDirectory(), 608 "maven-plugin-plugin-sources/" + sourcesArtifact.getGroupId() + "/" 609 + sourcesArtifact.getArtifactId() + "/" + sourcesArtifact.getVersion() 610 + "/" + sourcesArtifact.getClassifier()); 611 extractDirectory.mkdirs(); 612 613 UnArchiver unArchiver = archiverManager.getUnArchiver("jar"); 614 unArchiver.setSourceFile(sourcesArtifact.getFile()); 615 unArchiver.setDestDirectory(extractDirectory); 616 unArchiver.extract(); 617 618 extendJavaProjectBuilder(builder, Arrays.asList(extractDirectory), request.getDependencies()); 619 } else if (sourcesArtifact.getFile().isDirectory()) { 620 extendJavaProjectBuilder(builder, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies()); 621 } 622 } catch (ArchiverException | NoSuchArchiverException e) { 623 throw new ExtractionException(e.getMessage(), e); 624 } 625 } 626 627 private void extendJavaProjectBuilder(JavaProjectBuilder builder, final MavenProject project) { 628 List<File> sources = new ArrayList<>(); 629 630 for (String source : project.getCompileSourceRoots()) { 631 sources.add(new File(source)); 632 } 633 634 // TODO be more dynamic 635 File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin"); 636 if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath()) && generatedPlugin.exists()) { 637 sources.add(generatedPlugin); 638 } 639 extendJavaProjectBuilder(builder, sources, project.getArtifacts()); 640 } 641 642 private void extendJavaProjectBuilder( 643 JavaProjectBuilder builder, List<File> sourceDirectories, Set<Artifact> artifacts) { 644 645 // Build isolated Classloader with only the artifacts of the project (none of this plugin) 646 List<URL> urls = new ArrayList<>(artifacts.size()); 647 for (Artifact artifact : artifacts) { 648 try { 649 urls.add(artifact.getFile().toURI().toURL()); 650 } catch (MalformedURLException e) { 651 // noop 652 } 653 } 654 builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader())); 655 656 for (File source : sourceDirectories) { 657 builder.addSourceTree(source); 658 } 659 } 660 661 private List<MojoDescriptor> toMojoDescriptors( 662 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, PluginDescriptor pluginDescriptor) 663 throws InvalidPluginDescriptorException { 664 List<MojoDescriptor> mojoDescriptors = new ArrayList<>(mojoAnnotatedClasses.size()); 665 for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses.values()) { 666 // no mojo so skip it 667 if (mojoAnnotatedClass.getMojo() == null) { 668 continue; 669 } 670 671 ExtendedMojoDescriptor mojoDescriptor = new ExtendedMojoDescriptor(true); 672 673 // mojoDescriptor.setRole( mojoAnnotatedClass.getClassName() ); 674 // mojoDescriptor.setRoleHint( "default" ); 675 mojoDescriptor.setImplementation(mojoAnnotatedClass.getClassName()); 676 mojoDescriptor.setLanguage("java"); 677 678 mojoDescriptor.setV4Api(mojoAnnotatedClass.isV4Api()); 679 680 MojoAnnotationContent mojo = mojoAnnotatedClass.getMojo(); 681 682 mojoDescriptor.setDescription(mojo.getDescription()); 683 mojoDescriptor.setSince(mojo.getSince()); 684 mojo.setDeprecated(mojo.getDeprecated()); 685 686 mojoDescriptor.setProjectRequired(mojo.requiresProject()); 687 688 mojoDescriptor.setRequiresReports(mojo.requiresReports()); 689 690 mojoDescriptor.setComponentConfigurator(mojo.configurator()); 691 692 mojoDescriptor.setInheritedByDefault(mojo.inheritByDefault()); 693 694 mojoDescriptor.setInstantiationStrategy(mojo.instantiationStrategy().id()); 695 696 mojoDescriptor.setAggregator(mojo.aggregator()); 697 mojoDescriptor.setDependencyResolutionRequired( 698 mojo.requiresDependencyResolution().id()); 699 mojoDescriptor.setDependencyCollectionRequired( 700 mojo.requiresDependencyCollection().id()); 701 702 mojoDescriptor.setDirectInvocationOnly(mojo.requiresDirectInvocation()); 703 mojoDescriptor.setDeprecated(mojo.getDeprecated()); 704 mojoDescriptor.setThreadSafe(mojo.threadSafe()); 705 706 MojoAnnotatedClass mojoAnnotatedClassWithExecute = 707 findClassWithExecuteAnnotationInParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses); 708 if (mojoAnnotatedClassWithExecute != null && mojoAnnotatedClassWithExecute.getExecute() != null) { 709 ExecuteAnnotationContent execute = mojoAnnotatedClassWithExecute.getExecute(); 710 mojoDescriptor.setExecuteGoal(execute.goal()); 711 mojoDescriptor.setExecuteLifecycle(execute.lifecycle()); 712 if (execute.phase() != null) { 713 mojoDescriptor.setExecutePhase(execute.phase().id()); 714 if (StringUtils.isNotEmpty(execute.customPhase())) { 715 throw new InvalidPluginDescriptorException( 716 "@Execute annotation must only use either 'phase' " 717 + "or 'customPhase' but not both. Both are used though on " 718 + mojoAnnotatedClassWithExecute.getClassName(), 719 null); 720 } 721 } else if (StringUtils.isNotEmpty(execute.customPhase())) { 722 mojoDescriptor.setExecutePhase(execute.customPhase()); 723 } 724 } 725 726 mojoDescriptor.setExecutionStrategy(mojo.executionStrategy()); 727 // ??? 728 // mojoDescriptor.alwaysExecute(mojo.a) 729 730 mojoDescriptor.setGoal(mojo.name()); 731 mojoDescriptor.setOnlineRequired(mojo.requiresOnline()); 732 733 mojoDescriptor.setPhase(mojo.defaultPhase().id()); 734 735 // Parameter annotations 736 Map<String, ParameterAnnotationContent> parameters = 737 getParametersParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses); 738 739 for (ParameterAnnotationContent parameterAnnotationContent : new TreeSet<>(parameters.values())) { 740 org.apache.maven.plugin.descriptor.Parameter parameter = 741 new org.apache.maven.plugin.descriptor.Parameter(); 742 String name = StringUtils.isEmpty(parameterAnnotationContent.name()) 743 ? parameterAnnotationContent.getFieldName() 744 : parameterAnnotationContent.name(); 745 parameter.setName(name); 746 parameter.setAlias(parameterAnnotationContent.alias()); 747 parameter.setDefaultValue(parameterAnnotationContent.defaultValue()); 748 parameter.setDeprecated(parameterAnnotationContent.getDeprecated()); 749 parameter.setDescription(parameterAnnotationContent.getDescription()); 750 parameter.setEditable(!parameterAnnotationContent.readonly()); 751 String property = parameterAnnotationContent.property(); 752 if (StringUtils.contains(property, '$') 753 || StringUtils.contains(property, '{') 754 || StringUtils.contains(property, '}')) { 755 throw new InvalidParameterException( 756 "Invalid property for parameter '" + parameter.getName() + "', " 757 + "forbidden characters ${}: " + property, 758 null); 759 } 760 parameter.setExpression((property == null || property.isEmpty()) ? "" : "${" + property + "}"); 761 StringBuilder type = new StringBuilder(parameterAnnotationContent.getClassName()); 762 if (!parameterAnnotationContent.getTypeParameters().isEmpty()) { 763 type.append(parameterAnnotationContent.getTypeParameters().stream() 764 .collect(Collectors.joining(", ", "<", ">"))); 765 } 766 parameter.setType(type.toString()); 767 parameter.setSince(parameterAnnotationContent.getSince()); 768 parameter.setRequired(parameterAnnotationContent.required()); 769 770 mojoDescriptor.addParameter(parameter); 771 } 772 773 // Component annotations 774 Map<String, ComponentAnnotationContent> components = 775 getComponentsParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses); 776 777 for (ComponentAnnotationContent componentAnnotationContent : new TreeSet<>(components.values())) { 778 org.apache.maven.plugin.descriptor.Parameter parameter = 779 new org.apache.maven.plugin.descriptor.Parameter(); 780 parameter.setName(componentAnnotationContent.getFieldName()); 781 782 parameter.setRequirement(new Requirement( 783 componentAnnotationContent.getRoleClassName(), componentAnnotationContent.hint())); 784 parameter.setDeprecated(componentAnnotationContent.getDeprecated()); 785 parameter.setSince(componentAnnotationContent.getSince()); 786 787 // same behaviour as JavaJavadocMojoDescriptorExtractor 788 parameter.setEditable(false); 789 790 mojoDescriptor.addParameter(parameter); 791 } 792 793 mojoDescriptor.setPluginDescriptor(pluginDescriptor); 794 795 mojoDescriptors.add(mojoDescriptor); 796 } 797 return mojoDescriptors; 798 } 799 800 protected MojoAnnotatedClass findClassWithExecuteAnnotationInParentHierarchy( 801 MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) { 802 if (mojoAnnotatedClass.getExecute() != null) { 803 return mojoAnnotatedClass; 804 } 805 String parentClassName = mojoAnnotatedClass.getParentClassName(); 806 if (parentClassName == null || parentClassName.isEmpty()) { 807 return null; 808 } 809 MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName); 810 if (parent == null) { 811 return null; 812 } 813 return findClassWithExecuteAnnotationInParentHierarchy(parent, mojoAnnotatedClasses); 814 } 815 816 protected Map<String, ParameterAnnotationContent> getParametersParentHierarchy( 817 MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) { 818 List<ParameterAnnotationContent> parameterAnnotationContents = new ArrayList<>(); 819 820 parameterAnnotationContents = 821 getParametersParent(mojoAnnotatedClass, parameterAnnotationContents, mojoAnnotatedClasses); 822 823 // move to parent first to build the Map 824 Collections.reverse(parameterAnnotationContents); 825 826 Map<String, ParameterAnnotationContent> map = new HashMap<>(parameterAnnotationContents.size()); 827 828 for (ParameterAnnotationContent parameterAnnotationContent : parameterAnnotationContents) { 829 map.put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent); 830 } 831 return map; 832 } 833 834 protected List<ParameterAnnotationContent> getParametersParent( 835 MojoAnnotatedClass mojoAnnotatedClass, 836 List<ParameterAnnotationContent> parameterAnnotationContents, 837 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) { 838 parameterAnnotationContents.addAll(mojoAnnotatedClass.getParameters().values()); 839 String parentClassName = mojoAnnotatedClass.getParentClassName(); 840 if (parentClassName != null) { 841 MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName); 842 if (parent != null) { 843 return getParametersParent(parent, parameterAnnotationContents, mojoAnnotatedClasses); 844 } 845 } 846 return parameterAnnotationContents; 847 } 848 849 protected Map<String, ComponentAnnotationContent> getComponentsParentHierarchy( 850 MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) { 851 List<ComponentAnnotationContent> componentAnnotationContents = new ArrayList<>(); 852 853 componentAnnotationContents = 854 getComponentParent(mojoAnnotatedClass, componentAnnotationContents, mojoAnnotatedClasses); 855 856 // move to parent first to build the Map 857 Collections.reverse(componentAnnotationContents); 858 859 Map<String, ComponentAnnotationContent> map = new HashMap<>(componentAnnotationContents.size()); 860 861 for (ComponentAnnotationContent componentAnnotationContent : componentAnnotationContents) { 862 map.put(componentAnnotationContent.getFieldName(), componentAnnotationContent); 863 } 864 return map; 865 } 866 867 protected List<ComponentAnnotationContent> getComponentParent( 868 MojoAnnotatedClass mojoAnnotatedClass, 869 List<ComponentAnnotationContent> componentAnnotationContents, 870 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) { 871 componentAnnotationContents.addAll(mojoAnnotatedClass.getComponents().values()); 872 String parentClassName = mojoAnnotatedClass.getParentClassName(); 873 if (parentClassName != null) { 874 MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName); 875 if (parent != null) { 876 return getComponentParent(parent, componentAnnotationContents, mojoAnnotatedClasses); 877 } 878 } 879 return componentAnnotationContents; 880 } 881 882 protected MavenProject getFromProjectReferences(Artifact artifact, MavenProject project) { 883 if (project.getProjectReferences() == null 884 || project.getProjectReferences().isEmpty()) { 885 return null; 886 } 887 Collection<MavenProject> mavenProjects = project.getProjectReferences().values(); 888 for (MavenProject mavenProject : mavenProjects) { 889 if (Objects.equals(mavenProject.getId(), artifact.getId())) { 890 return mavenProject; 891 } 892 } 893 return null; 894 } 895}