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