View Javadoc
1   package org.codehaus.mojo.taglist;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.OutputStreamWriter;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.ResourceBundle;
32  import java.util.concurrent.atomic.AtomicReference;
33  
34  import org.apache.maven.doxia.siterenderer.Renderer;
35  import org.apache.maven.model.ReportPlugin;
36  import org.apache.maven.plugins.annotations.Mojo;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.plugins.annotations.ResolutionScope;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.reporting.AbstractMavenReport;
41  import org.apache.maven.reporting.MavenReportException;
42  import org.apache.maven.shared.utils.io.FileUtils;
43  import org.codehaus.mojo.taglist.beans.FileReport;
44  import org.codehaus.mojo.taglist.beans.TagReport;
45  import org.codehaus.mojo.taglist.output.TagListXMLComment;
46  import org.codehaus.mojo.taglist.output.TagListXMLFile;
47  import org.codehaus.mojo.taglist.output.TagListXMLReport;
48  import org.codehaus.mojo.taglist.output.TagListXMLTag;
49  import org.codehaus.mojo.taglist.output.io.xpp3.TaglistOutputXpp3Writer;
50  import org.codehaus.mojo.taglist.tags.AbsTag;
51  import org.codehaus.mojo.taglist.tags.InvalidTagException;
52  import org.codehaus.mojo.taglist.tags.TagClass;
53  import org.codehaus.mojo.taglist.tags.TagFactory;
54  import org.codehaus.plexus.util.IOUtil;
55  import org.codehaus.plexus.util.PathTool;
56  import org.codehaus.plexus.util.StringUtils;
57  
58  /**
59   * Scans the source files for tags and generates a report on their occurrences.
60   * 
61   * @author <a href="mailto:bellingard.NO-SPAM@gmail.com">Fabrice Bellingard</a>
62   */
63  @Mojo( name="taglist", requiresDependencyResolution = ResolutionScope.COMPILE)
64  public class TagListReport
65      extends AbstractMavenReport
66  {
67      /**
68       * Specifies the Locale of the source files.
69       * 
70       * @since 2.4
71       */
72      @Parameter( property = "sourceFileLocale", defaultValue = "en" )
73      private String sourceFileLocale;
74  
75      /** Default locale used if the source file locale is null. */
76      private static final String DEFAULT_LOCALE = "en";
77  
78      /**
79       * List of files to include. Specified as fileset patterns which are relative to the source directory.
80       *
81       * @since 3.0.0
82       */
83      @Parameter( defaultValue = "**/*.java" )
84      private String[] includes;
85  
86      /**
87       * List of files to exclude. Specified as fileset patterns which are relative to the source directory.
88       *
89       * @since 3.0.0
90       */
91      @Parameter( defaultValue = "" )
92      private String[] excludes;
93  
94      /**
95       * Specifies the directory where the xml output will be generated.
96       * 
97       * @since 2.3
98       */
99      @Parameter( defaultValue = "${project.build.directory}/taglist", required = true )
100     private File xmlOutputDirectory;
101 
102     /**
103      * This parameter indicates whether for simple tags (like "TODO"), the analyzer should look for multiple line
104      * comments.
105      * 
106      */
107     @Parameter( defaultValue = "true" )
108     private boolean multipleLineComments;
109 
110     /**
111      * This parameter indicates whether to look for tags even if they don't have a comment.
112      */
113     @Parameter( defaultValue = "true" )
114     private boolean emptyComments;
115 
116     /**
117      * Link the tag line numbers to the source xref. Defaults to true and will link automatically if jxr plugin is being
118      * used.
119      */
120     @Parameter( defaultValue = "true", property = "taglists.linkXRef")
121     private boolean linkXRef;
122 
123     /**
124      * Location of the Xrefs to link to.
125      */
126     @Parameter( defaultValue = "${project.reporting.outputDirectory}/xref", property = "taglists.xrefLocation" )
127     private File xrefLocation;
128 
129     /**
130      * Location of the Test Xrefs to link to.
131      */
132     @Parameter( defaultValue = "${project.reporting.outputDirectory}/xref-test", property = "taglists.testXrefLocation" )
133     private File testXrefLocation;
134 
135     /**
136      * The projects in the reactor for aggregation report.
137      */
138     @Parameter( readonly = true, defaultValue = "${reactorProjects}" )
139     private List reactorProjects;
140 
141     /**
142      * Whether to build an aggregated report at the root, or build individual reports.
143      */
144     @Parameter( defaultValue = "false", property = "taglists.aggregate" )
145     private boolean aggregate;
146 
147     /**
148      * The locale used for rendering the page.
149      */
150     private Locale currentLocale;
151 
152     /**
153      * This parameter indicates whether to generate details for tags with zero occurrences.
154      * 
155      * @since 2.2
156      */
157     @Parameter( defaultValue = "false" )
158     private boolean showEmptyDetails;
159 
160     /**
161      * Skips reporting of test sources.
162      * 
163      * @since 2.4
164      */
165     @Parameter( defaultValue = "false" )
166     private boolean skipTestSources;
167 
168     /**
169      * Defines each tag class (grouping) and the individual tags within each class. The user can also specify a title
170      * for each tag class and the matching logic used by each tag.
171      * <ul>
172      * <li><b>Exact Match</b> <br/>
173      * &lt;matchString&gt;todo&lt;/matchString&gt;<br/>
174      * &lt;matchType&gt;exact&lt;/matchType&gt; <br/>
175      * <i>Matches: todo </i></li>
176      * <li><b>Ignore Case Match</b> <br/>
177      * &lt;matchString&gt;todo&lt;/matchString&gt;<br/>
178      * &lt;matchType&gt;ignoreCase&lt;/matchType&gt; <br/>
179      * <i>Matches: todo, Todo, TODO... </i></li>
180      * <li><b>Regular Expression Match</b> <br/>
181      * &lt;matchString&gt;tod[aeo]&lt;/matchString&gt;<br/>
182      * &lt;matchType&gt;regEx&lt;/matchType&gt; <br/>
183      * <i>Matches: toda, tode, todo </i></li>
184      * </ul>
185      * <br/>
186      * <br/>
187      * For complete examples see the <a href="usage.html"><b>Usage</b></a> page. <br/>
188      *
189      * @since 2.4
190      */
191     @Parameter
192     private org.codehaus.mojo.taglist.options.TagListOptions tagListOptions;
193 
194     private String[] tags;
195 
196     private AtomicReference<List> sourceDirs = new AtomicReference<>();
197 
198     /**
199      * {@inheritDoc}
200      *
201      * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale)
202      */
203     protected void executeReport( Locale locale )
204             throws MavenReportException
205     {
206         this.currentLocale = locale;
207 
208         // User entered no tags and no tagOptions, then default tags
209         if ( ( tagListOptions == null || tagListOptions.getTagClasses().size() == 0 ) )
210         {
211             tags = new String[] { "@todo", "TODO" };
212         }
213 
214         if ( StringUtils.isEmpty( getInputEncoding() ) )
215         {
216             getLog().warn(
217                            "File encoding has not been set, using platform encoding "
218                                + System.getProperty( "file.encoding" ) + ", i.e. build is platform dependent!" );
219         }
220 
221         // Create the tag classes
222         List<TagClass> tagClasses = new ArrayList<>();
223 
224         // If any old style tags were used, then add each tag as a tag class
225         if ( tags != null && tags.length > 0 )
226         {
227             getLog().warn( "Using legacy tag format.  This is not recommended." );
228             for ( int i = 0; i < tags.length; i++ )
229             {
230                 TagClass tc = new TagClass( tags[i] );
231                 try
232                 {
233                     AbsTag newTag = TagFactory.createTag( "exact", tags[i] );
234                     tc.addTag( newTag );
235 
236                     tagClasses.add( tc );
237                 }
238                 catch ( InvalidTagException e )
239                 {
240                     // This should be impossible since exact is supported.
241                     getLog().error( "Invalid tag type used.  tag type: exact" );
242                 }
243             }
244         }
245 
246         // If the new style of tag options were used, add them
247         if ( tagListOptions != null && tagListOptions.getTagClasses().size() > 0 )
248         {
249             // Scan each tag class
250             Iterator classIter = tagListOptions.getTagClasses().iterator();
251             while ( classIter.hasNext() )
252             {
253                 org.codehaus.mojo.taglist.options.TagClass tcOption =
254                     (org.codehaus.mojo.taglist.options.TagClass) classIter.next();
255 
256                 // Store the tag class display name.
257                 TagClass tc = new TagClass( tcOption.getDisplayName() );
258 
259                 // Scan each tag within this tag class.
260                 Iterator tagIter = tcOption.getTags().iterator();
261                 while ( tagIter.hasNext() )
262                 {
263                     org.codehaus.mojo.taglist.options.Tag tagOption =
264                         (org.codehaus.mojo.taglist.options.Tag) tagIter.next();
265 
266                     // If a match type is not specified use default.
267                     String matchType = tagOption.getMatchType();
268                     if ( matchType == null || matchType.length() == 0 )
269                     {
270                         matchType = TagFactory.getDefaultTagType();
271                     }
272 
273                     try
274                     {
275                         // Create the tag based on the match type, and add it to the tag class
276                         AbsTag newTag = TagFactory.createTag( matchType, tagOption.getMatchString() );
277                         tc.addTag( newTag );
278                     }
279                     catch ( InvalidTagException e )
280                     {
281                         // This should be impossible since exact is supported.
282                         getLog().error( "Invalid tag type used.  tag type: " + matchType );
283                     }
284                 }
285 
286                 // Add this new tag class to the container.
287                 tagClasses.add( tc );
288             }
289         }
290 
291         // let's proceed to the analysis
292         FileAnalyser fileAnalyser = new FileAnalyser( this, tagClasses );
293         Collection tagReports = fileAnalyser.execute();
294 
295         // Renders the report
296         ReportGenerator generator = new ReportGenerator( this, tagReports );
297         if ( linkXRef )
298         {
299             String relativePath = getRelativePath( xrefLocation );
300             if ( xrefLocation.exists() )
301             {
302                 // XRef was already generated by manual execution of a lifecycle binding
303                 generator.setXrefLocation( relativePath );
304                 generator.setTestXrefLocation( getRelativePath( testXrefLocation ) );
305             }
306             else
307             {
308                 // Not yet generated - check if the report is on its way
309                 for ( Iterator reports = getProject().getReportPlugins().iterator(); reports.hasNext(); )
310                 {
311                     ReportPlugin report = (ReportPlugin) reports.next();
312 
313                     String artifactId = report.getArtifactId();
314                     if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) )
315                     {
316                         getLog().error(
317                                         "Taglist plugin MUST be executed after the JXR plugin."
318                                             + "  No links to xref were generated." );
319                     }
320                 }
321             }
322 
323             if ( generator.getXrefLocation() == null )
324             {
325                 getLog().warn( "Unable to locate Source XRef to link to - DISABLED" );
326             }
327         }
328         generator.generateReport();
329 
330         // Generate the XML report
331         generateXmlReport( tagReports );
332     }
333 
334     /**
335      * Generate an XML report that can be used by other plugins like the dashboard plugin.
336      * 
337      * @param tagReports a collection of the tag reports to be output.
338      */
339     private void generateXmlReport( Collection tagReports )
340     {
341         TagListXMLReport report = new TagListXMLReport();
342         report.setModelEncoding( getInputEncoding() );
343 
344         // Iterate through each tag and populate an XML tag object.
345         for ( Iterator ite = tagReports.iterator(); ite.hasNext(); )
346         {
347             TagReport tagReport = (TagReport) ite.next();
348 
349             TagListXMLTag tag = new TagListXMLTag();
350             tag.setName( tagReport.getTagName() );
351             tag.setCount( Integer.toString( tagReport.getTagCount() ) );
352 
353             // Iterate though each file that contains the current tag and generate an
354             // XML file object within the current XML tag object.
355             for ( Iterator fite = tagReport.getFileReports().iterator(); fite.hasNext(); )
356             {
357                 FileReport fileReport = (FileReport) fite.next();
358 
359                 TagListXMLFile file = new TagListXMLFile();
360                 file.setName( fileReport.getClassName() );
361                 file.setCount( Integer.toString( fileReport.getLineIndexes().size() ) );
362 
363                 // Iterate though each comment that contains the tag and generate an
364                 // XML comment object within the current xml file object.
365                 for ( Iterator cite = fileReport.getLineIndexes().iterator(); cite.hasNext(); )
366                 {
367                     Integer lineNumber = (Integer) cite.next();
368 
369                     TagListXMLComment comment = new TagListXMLComment();
370                     comment.setLineNumber( Integer.toString( lineNumber.intValue() ) );
371                     comment.setComment( fileReport.getComment( lineNumber ) );
372 
373                     file.addComment( comment );
374                 }
375                 tag.addFile( file );
376             }
377             report.addTag( tag );
378         }
379 
380         // Create the writer for the XML output file.
381         xmlOutputDirectory.mkdirs();
382         File xmlFile = new File( xmlOutputDirectory, "taglist.xml" );
383         FileOutputStream fos = null;
384         OutputStreamWriter output = null;
385 
386         try
387         {
388             fos = new FileOutputStream( xmlFile );
389             output = new OutputStreamWriter( fos, getInputEncoding());
390 
391             // Write out the XML output file.
392             TaglistOutputXpp3Writer xmlWriter = new TaglistOutputXpp3Writer();
393             xmlWriter.write( output, report );
394         }
395         catch ( Exception e )
396         {
397             getLog().warn( "Could not save taglist xml file: " + e.getMessage() );
398         }
399         finally
400         {
401             IOUtil.close( output );
402         }
403     }
404 
405     /**
406      * Returns the path relative to the output directory.
407      * 
408      * @param location the location to make relative.
409      * @return the relative path.
410      */
411     private String getRelativePath( File location )
412     {
413         String relativePath =
414             PathTool.getRelativePath( getReportOutputDirectory().getAbsolutePath(), location.getAbsolutePath() );
415         if ( StringUtils.isEmpty( relativePath ) )
416         {
417             relativePath = ".";
418         }
419         relativePath = relativePath + "/" + location.getName();
420         return relativePath;
421     }
422 
423     /**
424      * {@inheritDoc}
425      * 
426      * @see org.apache.maven.reporting.MavenReport#canGenerateReport()
427      */
428     public boolean canGenerateReport()
429     {
430         boolean canGenerate = !getSourceDirs().isEmpty();
431         if ( aggregate && !getProject().isExecutionRoot() )
432         {
433             canGenerate = false;
434         }
435         return canGenerate;
436     }
437 
438     /**
439      * Removes empty dirs from the list.
440      *
441      * @param sourceDirectories the original list of directories.
442      * @return a new list containing only non empty dirs.
443      */
444     private List pruneSourceDirs( List sourceDirectories ) throws IOException
445     {
446         List pruned = new ArrayList( sourceDirectories.size() );
447         for ( Iterator i = sourceDirectories.iterator(); i.hasNext(); )
448         {
449             String dir = (String) i.next();
450             if ( !pruned.contains( dir ) && hasSources( new File( dir ) ) )
451             {
452                 pruned.add( dir );
453             }
454         }
455         return pruned;
456     }
457 
458     /**
459      * Checks whether the given directory contains source files.
460      *
461      * @param dir the source directory.
462      * @return true if the folder or one of its subfolders contains at least 1 source file that matches
463      * includes/excludes.
464      */
465     private boolean hasSources( File dir ) throws IOException
466     {
467         if ( dir.exists() && dir.isDirectory() )
468         {
469             if ( !FileUtils.getFiles( dir, getIncludesCommaSeparated(), getExcludesCommaSeparated() ).isEmpty() )
470             {
471                 return true;
472             }
473 
474             File[] files = dir.listFiles();
475             if ( files != null )
476             {
477                 for ( int i = 0; i < files.length; i++ )
478                 {
479                     File currentFile = files[i];
480                     if ( currentFile.isDirectory() )
481                     {
482                         boolean hasSources = hasSources( currentFile );
483                         if ( hasSources )
484                         {
485                             return true;
486                         }
487                     }
488                 }
489             }
490         }
491         return false;
492     }
493 
494     /**
495      * Construct the list of source directories to analyze.
496      *
497      * @return the list of dirs.
498      */
499     private List constructSourceDirs()
500     {
501         List dirs = new ArrayList( getProject().getCompileSourceRoots() );
502         if ( !skipTestSources )
503         {
504             dirs.addAll( getProject().getTestCompileSourceRoots() );
505         }
506 
507         if ( aggregate )
508         {
509             for ( Iterator i = reactorProjects.iterator(); i.hasNext(); )
510             {
511                 MavenProject reactorProject = (MavenProject) i.next();
512 
513                 if ( "java".equals( reactorProject.getArtifact().getArtifactHandler().getLanguage() ) )
514                 {
515                     dirs.addAll( reactorProject.getCompileSourceRoots() );
516                     if ( !skipTestSources )
517                     {
518                         dirs.addAll( reactorProject.getTestCompileSourceRoots() );
519                     }
520                 }
521             }
522         }
523 
524         /*
525          * This try-catch is needed due to a missing declared exception in the
526          * 'canGenerateReport()' method. For this reason, neither the 'canGenerateReport()'
527          * nor the 'constructSourceDirs()' can throw exceptions.
528          * The exception itself is caused by a declaration from the FileUtils, but never used
529          * there. The FileUtils.getFiles() should be replaced by an NIO filter at some point.
530          */
531         try
532         {
533             dirs = pruneSourceDirs( dirs );
534         }
535         catch ( IOException javaIoIOException )
536         {
537             getLog().warn( "Unable to prune source dirs.", javaIoIOException );
538         }
539 
540         return dirs;
541     }
542 
543     protected List getSourceDirs()
544     {
545         if ( sourceDirs.get() == null )
546         {
547             sourceDirs.compareAndSet( null, constructSourceDirs() );
548         }
549 
550         return sourceDirs.get();
551     }
552 
553     /**
554      * Get the files to include, as a comma separated list of patterns.
555      */
556     String getIncludesCommaSeparated()
557     {
558         if ( includes != null )
559         {
560             return String.join( ",", includes );
561         }
562         else
563         {
564             return "";
565         }
566     }
567 
568     /**
569      * Get the files to exclude, as a comma separated list of patterns.
570      */
571     String getExcludesCommaSeparated()
572     {
573       if ( excludes != null ) {
574           return String.join(",", excludes);
575       } else {
576           return "";
577       }
578     }
579 
580   /**
581      * Returns the Locale of the source files.
582      * 
583      * @return The Locale of the source files.
584      */
585     public Locale getLocale()
586     {
587         // The locale string should never be null.
588         if ( sourceFileLocale == null )
589         {
590             sourceFileLocale = DEFAULT_LOCALE;
591         }
592         return new Locale( sourceFileLocale );
593     }
594 
595     /**
596      * Tells whether to look for comments over multiple lines.
597      * 
598      * @return Returns true if the analyzer should look for multiple lines.
599      */
600     public boolean isMultipleLineComments()
601     {
602         return multipleLineComments;
603     }
604 
605     /**
606      * Tells whether to look for tags without comments.
607      * 
608      * @return the emptyComments.
609      */
610     public boolean isEmptyComments()
611     {
612         return emptyComments;
613     }
614 
615     /**
616      * Tells whether to generate details for tags with zero occurrences.
617      * 
618      * @return the showEmptyTags.
619      */
620     public boolean isShowEmptyDetails()
621     {
622         return showEmptyDetails;
623     }
624 
625     /**
626      * {@inheritDoc}
627      * 
628      * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer()
629      */
630     protected Renderer getSiteRenderer()
631     {
632         return siteRenderer;
633     }
634 
635     /**
636      * {@inheritDoc}
637      * 
638      * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory()
639      */
640     protected String getOutputDirectory()
641     {
642         return outputDirectory.getAbsolutePath();
643     }
644 
645     /**
646      * Get the absolute path to the XML output directory.
647      * 
648      * @return string of the absolute path.
649      */
650     protected String getXMLOutputDirectory()
651     {
652         return xmlOutputDirectory.getAbsolutePath();
653     }
654 
655     /**
656      * {@inheritDoc}
657      * 
658      * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
659      */
660     public String getDescription( Locale locale )
661     {
662         return getBundle( locale ).getString( "report.taglist.description" );
663     }
664 
665     /**
666      * {@inheritDoc}
667      * 
668      * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
669      */
670     public String getName( Locale locale )
671     {
672         return getBundle( locale ).getString( "report.taglist.name" );
673     }
674 
675     /**
676      * {@inheritDoc}
677      * 
678      * @see org.apache.maven.reporting.MavenReport#getOutputName()
679      */
680     public String getOutputName()
681     {
682         return "taglist";
683     }
684 
685     /**
686      * Returns the correct resource bundle according to the locale.
687      * 
688      * @return the bundle corresponding to the locale used for rendering the report.
689      */
690     public ResourceBundle getBundle()
691     {
692         return getBundle( currentLocale );
693     }
694 
695     /**
696      * Returns the correct resource bundle according to the locale.
697      * 
698      * @param locale the locale of the user.
699      * @return the bundle corresponding to the locale.
700      */
701     private ResourceBundle getBundle( Locale locale )
702     {
703         return ResourceBundle.getBundle( "taglist-report", locale, this.getClass().getClassLoader() );
704     }
705 
706     @Override
707     protected String getInputEncoding()
708     {
709         return super.getInputEncoding();
710     }
711 }