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