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.javadoc; 020 021import javax.inject.Named; 022import javax.inject.Singleton; 023 024import java.io.File; 025import java.net.MalformedURLException; 026import java.net.URL; 027import java.net.URLClassLoader; 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.List; 031import java.util.Map; 032import java.util.TreeMap; 033 034import com.thoughtworks.qdox.JavaProjectBuilder; 035import com.thoughtworks.qdox.library.SortedClassLibraryBuilder; 036import com.thoughtworks.qdox.model.DocletTag; 037import com.thoughtworks.qdox.model.JavaClass; 038import com.thoughtworks.qdox.model.JavaField; 039import com.thoughtworks.qdox.model.JavaType; 040import org.apache.maven.artifact.Artifact; 041import org.apache.maven.plugin.descriptor.InvalidParameterException; 042import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException; 043import org.apache.maven.plugin.descriptor.MojoDescriptor; 044import org.apache.maven.plugin.descriptor.Parameter; 045import org.apache.maven.plugin.descriptor.Requirement; 046import org.apache.maven.project.MavenProject; 047import org.apache.maven.tools.plugin.ExtendedMojoDescriptor; 048import org.apache.maven.tools.plugin.PluginToolsRequest; 049import org.apache.maven.tools.plugin.extractor.ExtractionException; 050import org.apache.maven.tools.plugin.extractor.GroupKey; 051import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor; 052import org.apache.maven.tools.plugin.util.PluginUtils; 053import org.codehaus.plexus.logging.AbstractLogEnabled; 054import org.codehaus.plexus.util.StringUtils; 055 056/** 057 * <p> 058 * Extracts Mojo descriptors from <a href="https://www.oracle.com/java/technologies//">Java</a> source 059 * javadoc comments only. New mojos should rather rely on annotations and comments which are evaluated 060 * by extractor named {@code java}. 061 * </p> 062 * For more information about the usage tag, have a look to: 063 * <a href="https://maven.apache.org/developers/mojo-api-specification.html"> 064 * https://maven.apache.org/developers/mojo-api-specification.html</a> 065 * 066 * @see org.apache.maven.plugin.descriptor.MojoDescriptor 067 */ 068@Named(JavaJavadocMojoDescriptorExtractor.NAME) 069@Singleton 070public class JavaJavadocMojoDescriptorExtractor extends AbstractLogEnabled 071 implements MojoDescriptorExtractor, JavadocMojoAnnotation { 072 public static final String NAME = "java-javadoc"; 073 074 private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 200); 075 076 @Override 077 public String getName() { 078 return NAME; 079 } 080 081 @Override 082 public boolean isDeprecated() { 083 return true; // one should use Java5 annotations instead 084 } 085 086 @Override 087 public GroupKey getGroupKey() { 088 return GROUP_KEY; 089 } 090 091 /** 092 * @param parameter not null 093 * @param i positive number 094 * @throws InvalidParameterException if any 095 */ 096 protected void validateParameter(Parameter parameter, int i) throws InvalidParameterException { 097 // TODO: remove when backward compatibility is no longer an issue. 098 String name = parameter.getName(); 099 100 if (name == null) { 101 throw new InvalidParameterException("name", i); 102 } 103 104 // TODO: remove when backward compatibility is no longer an issue. 105 String type = parameter.getType(); 106 107 if (type == null) { 108 throw new InvalidParameterException("type", i); 109 } 110 111 // TODO: remove when backward compatibility is no longer an issue. 112 String description = parameter.getDescription(); 113 114 if (description == null) { 115 throw new InvalidParameterException("description", i); 116 } 117 } 118 119 // ---------------------------------------------------------------------- 120 // Mojo descriptor creation from @tags 121 // ---------------------------------------------------------------------- 122 123 /** 124 * @param javaClass not null 125 * @return a mojo descriptor 126 * @throws InvalidPluginDescriptorException if any 127 */ 128 protected MojoDescriptor createMojoDescriptor(JavaClass javaClass) throws InvalidPluginDescriptorException { 129 ExtendedMojoDescriptor mojoDescriptor = new ExtendedMojoDescriptor(); 130 mojoDescriptor.setLanguage("java"); 131 mojoDescriptor.setImplementation(javaClass.getFullyQualifiedName()); 132 mojoDescriptor.setDescription(javaClass.getComment()); 133 134 // ---------------------------------------------------------------------- 135 // Mojo annotations in alphabetical order 136 // ---------------------------------------------------------------------- 137 138 // Aggregator flag 139 DocletTag aggregator = findInClassHierarchy(javaClass, JavadocMojoAnnotation.AGGREGATOR); 140 if (aggregator != null) { 141 mojoDescriptor.setAggregator(true); 142 } 143 144 // Configurator hint 145 DocletTag configurator = findInClassHierarchy(javaClass, JavadocMojoAnnotation.CONFIGURATOR); 146 if (configurator != null) { 147 mojoDescriptor.setComponentConfigurator(configurator.getValue()); 148 } 149 150 // Additional phase to execute first 151 DocletTag execute = findInClassHierarchy(javaClass, JavadocMojoAnnotation.EXECUTE); 152 if (execute != null) { 153 String executePhase = execute.getNamedParameter(JavadocMojoAnnotation.EXECUTE_PHASE); 154 String executeGoal = execute.getNamedParameter(JavadocMojoAnnotation.EXECUTE_GOAL); 155 156 if (executePhase == null && executeGoal == null) { 157 throw new InvalidPluginDescriptorException(javaClass.getFullyQualifiedName() 158 + ": @execute tag requires either a 'phase' or 'goal' parameter"); 159 } else if (executePhase != null && executeGoal != null) { 160 throw new InvalidPluginDescriptorException(javaClass.getFullyQualifiedName() 161 + ": @execute tag can have only one of a 'phase' or 'goal' parameter"); 162 } 163 mojoDescriptor.setExecutePhase(executePhase); 164 mojoDescriptor.setExecuteGoal(executeGoal); 165 166 String lifecycle = execute.getNamedParameter(JavadocMojoAnnotation.EXECUTE_LIFECYCLE); 167 if (lifecycle != null) { 168 mojoDescriptor.setExecuteLifecycle(lifecycle); 169 if (mojoDescriptor.getExecuteGoal() != null) { 170 throw new InvalidPluginDescriptorException(javaClass.getFullyQualifiedName() 171 + ": @execute lifecycle requires a phase instead of a goal"); 172 } 173 } 174 } 175 176 // Goal name 177 DocletTag goal = findInClassHierarchy(javaClass, JavadocMojoAnnotation.GOAL); 178 if (goal != null) { 179 mojoDescriptor.setGoal(goal.getValue()); 180 } 181 182 // inheritByDefault flag 183 boolean value = getBooleanTagValue( 184 javaClass, JavadocMojoAnnotation.INHERIT_BY_DEFAULT, mojoDescriptor.isInheritedByDefault()); 185 mojoDescriptor.setInheritedByDefault(value); 186 187 // instantiationStrategy 188 DocletTag tag = findInClassHierarchy(javaClass, JavadocMojoAnnotation.INSTANTIATION_STRATEGY); 189 if (tag != null) { 190 mojoDescriptor.setInstantiationStrategy(tag.getValue()); 191 } 192 193 // executionStrategy (and deprecated @attainAlways) 194 tag = findInClassHierarchy(javaClass, JavadocMojoAnnotation.MULTI_EXECUTION_STRATEGY); 195 if (tag != null) { 196 getLogger() 197 .warn("@" + JavadocMojoAnnotation.MULTI_EXECUTION_STRATEGY + " in " 198 + javaClass.getFullyQualifiedName() + " is deprecated: please use '@" 199 + JavadocMojoAnnotation.EXECUTION_STATEGY + " always' instead."); 200 mojoDescriptor.setExecutionStrategy(MojoDescriptor.MULTI_PASS_EXEC_STRATEGY); 201 } else { 202 mojoDescriptor.setExecutionStrategy(MojoDescriptor.SINGLE_PASS_EXEC_STRATEGY); 203 } 204 tag = findInClassHierarchy(javaClass, JavadocMojoAnnotation.EXECUTION_STATEGY); 205 if (tag != null) { 206 mojoDescriptor.setExecutionStrategy(tag.getValue()); 207 } 208 209 // Phase name 210 DocletTag phase = findInClassHierarchy(javaClass, JavadocMojoAnnotation.PHASE); 211 if (phase != null) { 212 mojoDescriptor.setPhase(phase.getValue()); 213 } 214 215 // Dependency resolution flag 216 DocletTag requiresDependencyResolution = 217 findInClassHierarchy(javaClass, JavadocMojoAnnotation.REQUIRES_DEPENDENCY_RESOLUTION); 218 if (requiresDependencyResolution != null) { 219 String v = requiresDependencyResolution.getValue(); 220 221 if (StringUtils.isEmpty(v)) { 222 v = "runtime"; 223 } 224 225 mojoDescriptor.setDependencyResolutionRequired(v); 226 } 227 228 // Dependency collection flag 229 DocletTag requiresDependencyCollection = 230 findInClassHierarchy(javaClass, JavadocMojoAnnotation.REQUIRES_DEPENDENCY_COLLECTION); 231 if (requiresDependencyCollection != null) { 232 String v = requiresDependencyCollection.getValue(); 233 234 if (StringUtils.isEmpty(v)) { 235 v = "runtime"; 236 } 237 238 mojoDescriptor.setDependencyCollectionRequired(v); 239 } 240 241 // requiresDirectInvocation flag 242 value = getBooleanTagValue( 243 javaClass, JavadocMojoAnnotation.REQUIRES_DIRECT_INVOCATION, mojoDescriptor.isDirectInvocationOnly()); 244 mojoDescriptor.setDirectInvocationOnly(value); 245 246 // Online flag 247 value = getBooleanTagValue(javaClass, JavadocMojoAnnotation.REQUIRES_ONLINE, mojoDescriptor.isOnlineRequired()); 248 mojoDescriptor.setOnlineRequired(value); 249 250 // Project flag 251 value = getBooleanTagValue( 252 javaClass, JavadocMojoAnnotation.REQUIRES_PROJECT, mojoDescriptor.isProjectRequired()); 253 mojoDescriptor.setProjectRequired(value); 254 255 // requiresReports flag 256 value = getBooleanTagValue( 257 javaClass, JavadocMojoAnnotation.REQUIRES_REPORTS, mojoDescriptor.isRequiresReports()); 258 mojoDescriptor.setRequiresReports(value); 259 260 // ---------------------------------------------------------------------- 261 // Javadoc annotations in alphabetical order 262 // ---------------------------------------------------------------------- 263 264 // Deprecation hint 265 DocletTag deprecated = javaClass.getTagByName(JavadocMojoAnnotation.DEPRECATED); 266 if (deprecated != null) { 267 mojoDescriptor.setDeprecated(deprecated.getValue()); 268 } 269 270 // What version it was introduced in 271 DocletTag since = findInClassHierarchy(javaClass, JavadocMojoAnnotation.SINCE); 272 if (since != null) { 273 mojoDescriptor.setSince(since.getValue()); 274 } 275 276 // Thread-safe mojo 277 278 value = getBooleanTagValue(javaClass, JavadocMojoAnnotation.THREAD_SAFE, true, mojoDescriptor.isThreadSafe()); 279 mojoDescriptor.setThreadSafe(value); 280 281 extractParameters(mojoDescriptor, javaClass); 282 283 return mojoDescriptor; 284 } 285 286 /** 287 * @param javaClass not null 288 * @param tagName not null 289 * @param defaultValue the wanted default value 290 * @return the boolean value of the given tagName 291 * @see #findInClassHierarchy(JavaClass, String) 292 */ 293 private static boolean getBooleanTagValue(JavaClass javaClass, String tagName, boolean defaultValue) { 294 DocletTag tag = findInClassHierarchy(javaClass, tagName); 295 296 if (tag != null) { 297 String value = tag.getValue(); 298 299 if (StringUtils.isNotEmpty(value)) { 300 defaultValue = Boolean.valueOf(value).booleanValue(); 301 } 302 } 303 return defaultValue; 304 } 305 306 /** 307 * @param javaClass not null 308 * @param tagName not null 309 * @param defaultForTag The wanted default value when only the tagname is present 310 * @param defaultValue the wanted default value when the tag is not specified 311 * @return the boolean value of the given tagName 312 * @see #findInClassHierarchy(JavaClass, String) 313 */ 314 private static boolean getBooleanTagValue( 315 JavaClass javaClass, String tagName, boolean defaultForTag, boolean defaultValue) { 316 DocletTag tag = findInClassHierarchy(javaClass, tagName); 317 318 if (tag != null) { 319 String value = tag.getValue(); 320 321 if (StringUtils.isNotEmpty(value)) { 322 return Boolean.valueOf(value).booleanValue(); 323 } else { 324 return defaultForTag; 325 } 326 } 327 return defaultValue; 328 } 329 330 /** 331 * @param javaClass not null 332 * @param tagName not null 333 * @return docletTag instance 334 */ 335 private static DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) { 336 DocletTag tag = javaClass.getTagByName(tagName); 337 338 if (tag == null) { 339 JavaClass superClass = javaClass.getSuperJavaClass(); 340 341 if (superClass != null) { 342 tag = findInClassHierarchy(superClass, tagName); 343 } 344 } 345 346 return tag; 347 } 348 349 /** 350 * @param mojoDescriptor not null 351 * @param javaClass not null 352 * @throws InvalidPluginDescriptorException if any 353 */ 354 private void extractParameters(MojoDescriptor mojoDescriptor, JavaClass javaClass) 355 throws InvalidPluginDescriptorException { 356 // --------------------------------------------------------------------------------- 357 // We're resolving class-level, ancestor-class-field, local-class-field order here. 358 // --------------------------------------------------------------------------------- 359 360 Map<String, JavaField> rawParams = extractFieldParameterTags(javaClass); 361 362 for (Map.Entry<String, JavaField> entry : rawParams.entrySet()) { 363 JavaField field = entry.getValue(); 364 365 JavaType type = field.getType(); 366 367 Parameter pd = new Parameter(); 368 369 pd.setName(entry.getKey()); 370 371 pd.setType(type.getFullyQualifiedName()); 372 373 pd.setDescription(field.getComment()); 374 375 DocletTag deprecationTag = field.getTagByName(JavadocMojoAnnotation.DEPRECATED); 376 377 if (deprecationTag != null) { 378 pd.setDeprecated(deprecationTag.getValue()); 379 } 380 381 DocletTag sinceTag = field.getTagByName(JavadocMojoAnnotation.SINCE); 382 if (sinceTag != null) { 383 pd.setSince(sinceTag.getValue()); 384 } 385 386 DocletTag componentTag = field.getTagByName(JavadocMojoAnnotation.COMPONENT); 387 388 if (componentTag != null) { 389 // Component tag 390 String role = componentTag.getNamedParameter(JavadocMojoAnnotation.COMPONENT_ROLE); 391 392 if (role == null) { 393 role = field.getType().toString(); 394 } 395 396 String roleHint = componentTag.getNamedParameter(JavadocMojoAnnotation.COMPONENT_ROLEHINT); 397 398 if (roleHint == null) { 399 // support alternate syntax for better compatibility with the Plexus CDC. 400 roleHint = componentTag.getNamedParameter("role-hint"); 401 } 402 403 // recognize Maven-injected objects as components annotations instead of parameters 404 // Note: the expressions we are looking for, i.e. "${project}", are in the values of the Map, 405 // so the lookup mechanism is different here than in maven-plugin-tools-annotations 406 boolean isDeprecated = PluginUtils.MAVEN_COMPONENTS.containsValue(role); 407 408 if (!isDeprecated) { 409 // normal component 410 pd.setRequirement(new Requirement(role, roleHint)); 411 } else { 412 // not a component but a Maven object to be transformed into an expression/property 413 getLogger() 414 .warn("Deprecated @component Javadoc tag for '" + pd.getName() + "' field in " 415 + javaClass.getFullyQualifiedName() 416 + ": replace with @Parameter( defaultValue = \"" + role 417 + "\", readonly = true )"); 418 pd.setDefaultValue(role); 419 pd.setRequired(true); 420 } 421 422 pd.setEditable(false); 423 /* TODO: or better like this? Need @component fields be editable for the user? 424 pd.setEditable( field.getTagByName( READONLY ) == null ); 425 */ 426 } else { 427 // Parameter tag 428 DocletTag parameter = field.getTagByName(JavadocMojoAnnotation.PARAMETER); 429 430 pd.setRequired(field.getTagByName(JavadocMojoAnnotation.REQUIRED) != null); 431 432 pd.setEditable(field.getTagByName(JavadocMojoAnnotation.READONLY) == null); 433 434 String name = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_NAME); 435 436 if (!StringUtils.isEmpty(name)) { 437 pd.setName(name); 438 } 439 440 String alias = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_ALIAS); 441 442 if (!StringUtils.isEmpty(alias)) { 443 pd.setAlias(alias); 444 } 445 446 String expression = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_EXPRESSION); 447 String property = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_PROPERTY); 448 449 if (StringUtils.isNotEmpty(expression) && StringUtils.isNotEmpty(property)) { 450 getLogger().error(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":"); 451 getLogger().error(" Cannot use both:"); 452 getLogger().error(" @parameter expression=\"${property}\""); 453 getLogger().error(" and"); 454 getLogger().error(" @parameter property=\"property\""); 455 getLogger().error(" Second syntax is preferred."); 456 throw new InvalidParameterException( 457 javaClass.getFullyQualifiedName() + "#" + field.getName() + ": cannot" 458 + " use both @parameter expression and property", 459 null); 460 } 461 462 if (StringUtils.isNotEmpty(expression)) { 463 getLogger().warn(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":"); 464 getLogger().warn(" The syntax"); 465 getLogger().warn(" @parameter expression=\"${property}\""); 466 getLogger().warn(" is deprecated, please use"); 467 getLogger().warn(" @parameter property=\"property\""); 468 getLogger().warn(" instead."); 469 470 } else if (StringUtils.isNotEmpty(property)) { 471 expression = "${" + property + "}"; 472 } 473 474 pd.setExpression(expression); 475 476 if (StringUtils.isNotEmpty(expression) && expression.startsWith("${component.")) { 477 getLogger().warn(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":"); 478 getLogger().warn(" The syntax"); 479 getLogger().warn(" @parameter expression=\"${component.<role>#<roleHint>}\""); 480 getLogger().warn(" is deprecated, please use"); 481 getLogger().warn(" @component role=\"<role>\" roleHint=\"<roleHint>\""); 482 getLogger().warn(" instead."); 483 } 484 485 if ("${reports}".equals(pd.getExpression())) { 486 mojoDescriptor.setRequiresReports(true); 487 } 488 489 pd.setDefaultValue(parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_DEFAULT_VALUE)); 490 491 pd.setImplementation(parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_IMPLEMENTATION)); 492 } 493 494 mojoDescriptor.addParameter(pd); 495 } 496 } 497 498 /** 499 * extract fields that are either parameters or components. 500 * 501 * @param javaClass not null 502 * @return map with Mojo parameters names as keys 503 */ 504 private Map<String, JavaField> extractFieldParameterTags(JavaClass javaClass) { 505 Map<String, JavaField> rawParams; 506 507 // we have to add the parent fields first, so that they will be overwritten by the local fields if 508 // that actually happens... 509 JavaClass superClass = javaClass.getSuperJavaClass(); 510 511 if (superClass != null) { 512 rawParams = extractFieldParameterTags(superClass); 513 } else { 514 rawParams = new TreeMap<String, JavaField>(); 515 } 516 517 for (JavaField field : javaClass.getFields()) { 518 if (field.getTagByName(JavadocMojoAnnotation.PARAMETER) != null 519 || field.getTagByName(JavadocMojoAnnotation.COMPONENT) != null) { 520 rawParams.put(field.getName(), field); 521 } 522 } 523 return rawParams; 524 } 525 526 @Override 527 public List<MojoDescriptor> execute(PluginToolsRequest request) 528 throws ExtractionException, InvalidPluginDescriptorException { 529 Collection<JavaClass> javaClasses = discoverClasses(request); 530 531 List<MojoDescriptor> descriptors = new ArrayList<>(); 532 533 for (JavaClass javaClass : javaClasses) { 534 DocletTag tag = javaClass.getTagByName(GOAL); 535 536 if (tag != null) { 537 MojoDescriptor mojoDescriptor = createMojoDescriptor(javaClass); 538 mojoDescriptor.setPluginDescriptor(request.getPluginDescriptor()); 539 540 // Validate the descriptor as best we can before allowing it to be processed. 541 validate(mojoDescriptor); 542 543 descriptors.add(mojoDescriptor); 544 } 545 } 546 547 return descriptors; 548 } 549 550 /** 551 * @param request The plugin request. 552 * @return an array of java class 553 */ 554 protected Collection<JavaClass> discoverClasses(final PluginToolsRequest request) { 555 JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder()); 556 builder.setEncoding(request.getEncoding()); 557 558 // Build isolated Classloader with only the artifacts of the project (none of this plugin) 559 List<URL> urls = new ArrayList<>(request.getDependencies().size()); 560 for (Artifact artifact : request.getDependencies()) { 561 try { 562 urls.add(artifact.getFile().toURI().toURL()); 563 } catch (MalformedURLException e) { 564 // noop 565 } 566 } 567 builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader())); 568 569 MavenProject project = request.getProject(); 570 571 for (String source : project.getCompileSourceRoots()) { 572 builder.addSourceTree(new File(source)); 573 } 574 575 // TODO be more dynamic 576 File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin"); 577 if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath())) { 578 builder.addSourceTree(generatedPlugin); 579 } 580 581 return builder.getClasses(); 582 } 583 584 /** 585 * @param mojoDescriptor not null 586 * @throws InvalidParameterException if any 587 */ 588 protected void validate(MojoDescriptor mojoDescriptor) throws InvalidParameterException { 589 List<Parameter> parameters = mojoDescriptor.getParameters(); 590 591 if (parameters != null) { 592 for (int j = 0; j < parameters.size(); j++) { 593 validateParameter(parameters.get(j), j); 594 } 595 } 596 } 597}