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