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