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.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.LineNumberReader;
27  import java.io.Reader;
28  import java.nio.file.Files;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.List;
32  import java.util.Locale;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.maven.plugin.logging.Log;
36  import org.codehaus.mojo.taglist.beans.FileReport;
37  import org.codehaus.mojo.taglist.beans.TagReport;
38  import org.codehaus.mojo.taglist.tags.TagClass;
39  import org.codehaus.plexus.util.FileUtils;
40  
41  /**
42   * Class that analyzes a file with a special comment tag. For instance:
43   *
44   * <pre>
45   * // TODO: Example of an Eclipse/IntelliJ-like "todo" tag
46   * </pre>
47   *
48   * @author <a href="mailto:bellingard.NO-SPAM@gmail.com">Fabrice Bellingard </a>
49   * @todo : This is another example of "todo" tag
50   */
51  public class FileAnalyser {
52      /**
53       * String that is used for beginning a comment line.
54       */
55      private static final String STAR_COMMENT = "*";
56  
57      /**
58       * String that is used for beginning a comment line.
59       */
60      private static final String SLASH_COMMENT = "//";
61  
62      /**
63       * Maximum length of a comment.
64       */
65      private static final int MAX_COMMENT_CHARACTERS = 99999;
66  
67      /**
68       * The character encoding of the files to analyze.
69       */
70      private final String encoding;
71  
72      /**
73       * The Locale of the files to analyze.
74       */
75      private final Locale sourceFileLocale;
76  
77      /**
78       * The directories to analyze.
79       */
80      private final Collection<String> sourceDirs;
81  
82      /**
83       * The files to include, as a comma separated list of patterns.
84       */
85      private final String includes;
86  
87      /**
88       * The files top exclude, as a comma separated list of patterns.
89       */
90      private final String excludes;
91  
92      /**
93       * Log for debug output.
94       */
95      private final Log log;
96  
97      /**
98       * Set to true if the analyzer should look for multiple line comments.
99       */
100     private final boolean multipleLineCommentsOn;
101 
102     /**
103      * Set to true if the analyzer should look for tags without comments.
104      */
105     private final boolean emptyCommentsOn;
106 
107     /**
108      * ArrayList of tag classes.
109      */
110     private final List<TagClass> tagClasses;
111 
112     /**
113      * Constructor.
114      *
115      * @param report the MOJO that is using this analyzer.
116      * @param tagClasses the array of tag classes to use for searching
117      */
118     public FileAnalyser(TagListReport report, List<TagClass> tagClasses) {
119         // TODO - direct class TagListReport should not be used,
120         //  we can add a separate args or new class/interface for group of args
121         multipleLineCommentsOn = report.isMultipleLineComments();
122         emptyCommentsOn = report.isEmptyComments();
123         log = report.getLog();
124         sourceDirs = report.getSourceDirs();
125         encoding = report.getInputEncoding();
126         sourceFileLocale = report.getSourceFileLocale();
127         this.tagClasses = tagClasses;
128         this.includes = report.getIncludesCommaSeparated();
129         this.excludes = report.getExcludesCommaSeparated();
130     }
131 
132     /**
133      * Execute the analysis for the configuration given by the TagListReport.
134      *
135      * @return a collection of TagReport objects.
136      */
137     public Collection<TagReport> execute() throws IOException {
138         List<File> fileList = findFilesToScan();
139 
140         for (File file : fileList) {
141             if (file.exists()) {
142                 scanFile(file);
143             }
144         }
145 
146         // Get the tag reports from each of the tag classes.
147         Collection<TagReport> tagReports = new ArrayList<>();
148         for (TagClass tc : tagClasses) {
149             tagReports.add(tc.getTagReport());
150         }
151 
152         return tagReports;
153     }
154 
155     /**
156      * Gives the list of files to scan.
157      *
158      * @return a List of File objects.
159      */
160     private List<File> findFilesToScan() throws IOException {
161         List<File> filesList = new ArrayList<>();
162         for (String sourceDir : sourceDirs) {
163             filesList.addAll(FileUtils.getFiles(new File(sourceDir), includes, excludes));
164         }
165         return filesList;
166     }
167 
168     /**
169      * Access an input reader that uses the current file encoding.
170      *
171      * @param file the file to open in the reader.
172      * @return a reader with the current file encoding.
173      * @throws IOException the IO exception.
174      */
175     private Reader getReader(File file) throws IOException {
176         InputStream in = Files.newInputStream(file.toPath());
177         return (encoding == null) ? new InputStreamReader(in) : new InputStreamReader(in, encoding);
178     }
179 
180     /**
181      * Scans a file to look for task tags.
182      *
183      * @param file the file to scan.
184      */
185     public void scanFile(File file) {
186         try (LineNumberReader reader = new LineNumberReader(getReader(file))) {
187 
188             String currentLine = reader.readLine();
189             while (currentLine != null) {
190                 int index;
191                 // look for a tag on this line
192                 for (TagClass tagClass : tagClasses) {
193                     index = tagClass.tagMatchContains(currentLine, sourceFileLocale);
194                     if (index != TagClass.NO_MATCH) {
195                         // there's a tag on this line
196                         String commentType = extractCommentType(currentLine, index);
197                         if (commentType == null) {
198                             // this is not a valid comment tag: skip other tag classes and
199                             // go to the next line
200                             break;
201                         }
202 
203                         int tagLength = tagClass.getLastTagMatchStringLength();
204                         int commentStartIndex = reader.getLineNumber();
205                         StringBuilder comment = new StringBuilder();
206 
207                         String firstLine = StringUtils.strip(currentLine.substring(index + tagLength));
208                         firstLine = StringUtils.removeEnd(firstLine, "*/"); // MTAGLIST-35
209                         if (firstLine.isEmpty() || ":".equals(firstLine)) {
210                             // this is not a valid comment tag: nothing is written there
211                             if (!emptyCommentsOn) {
212                                 continue;
213                             }
214                         } else {
215                             // this tag has a comment
216                             if (firstLine.charAt(0) == ':') {
217                                 comment.append(firstLine.substring(1).trim());
218                             } else {
219                                 comment.append(firstLine);
220                             }
221 
222                             if (multipleLineCommentsOn) {
223                                 // Mark the current position, set the read forward limit to
224                                 // a large number that should not be met.
225                                 reader.mark(MAX_COMMENT_CHARACTERS);
226 
227                                 // next line
228                                 String futureLine = reader.readLine();
229 
230                                 // we're looking for multiple line comments
231                                 while (futureLine != null
232                                         && futureLine.trim().startsWith(commentType)
233                                         && !futureLine.contains(tagClass.getLastTagMatchString())) {
234                                     String currentComment = futureLine
235                                             .substring(futureLine.indexOf(commentType) + commentType.length())
236                                             .trim();
237                                     if (currentComment.startsWith("@")
238                                             || currentComment.isEmpty()
239                                             || "/".equals(currentComment)) {
240                                         // the comment is finished
241                                         break;
242                                     }
243                                     // try to look if the next line is not a new tag
244                                     boolean newTagFound = false;
245                                     for (TagClass tc : tagClasses) {
246                                         if (tc.tagMatchStartsWith(currentComment, sourceFileLocale)) {
247                                             newTagFound = true;
248                                             break;
249                                         }
250                                     }
251                                     if (newTagFound) {
252                                         // this is a new comment: stop here the current comment
253                                         break;
254                                     }
255                                     // nothing was found: this means the comment is going on this line
256                                     comment.append(" ");
257                                     comment.append(currentComment);
258                                     futureLine = reader.readLine();
259                                 }
260 
261                                 // Reset the reader to the marked position before the multi
262                                 // line check was performed.
263                                 reader.reset();
264                             }
265                         }
266                         TagReport tagReport = tagClass.getTagReport();
267                         FileReport fileReport = tagReport.getFileReport(file, encoding);
268                         fileReport.addComment(comment.toString(), commentStartIndex);
269                     }
270                 }
271                 currentLine = reader.readLine();
272             }
273         } catch (IOException e) {
274             log.error("Error while scanning the file " + file.getPath(), e);
275         }
276     }
277 
278     /**
279      * Finds the type of comment the tag is in.
280      *
281      * @param currentLine the line to analyze.
282      * @param index the index of the tag in the line.
283      * @return "*" or "//" or null.
284      */
285     private String extractCommentType(String currentLine, int index) {
286         String commentType = null;
287         String beforeTag = currentLine.substring(0, index).trim();
288         if (beforeTag.endsWith(SLASH_COMMENT)) {
289             commentType = SLASH_COMMENT;
290         } else if (beforeTag.endsWith(STAR_COMMENT)) {
291             commentType = STAR_COMMENT;
292         }
293         return commentType;
294     }
295 }