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