View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.api;
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.Reader;
29  import java.io.StringReader;
30  import java.nio.charset.Charset;
31  import java.nio.charset.CharsetDecoder;
32  import java.nio.charset.CodingErrorAction;
33  import java.nio.charset.UnsupportedCharsetException;
34  import java.nio.file.Files;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.List;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  /**
42   * Represents the text contents of a file of arbitrary plain text type.
43   *
44   * <p>
45   * This class will be passed to instances of class FileSetCheck by
46   * Checker.
47   * </p>
48   *
49   */
50  public final class FileText {
51  
52      /**
53       * The number of characters to read in one go.
54       */
55      private static final int READ_BUFFER_SIZE = 1024;
56  
57      /**
58       * Regular expression pattern matching all line terminators.
59       */
60      private static final Pattern LINE_TERMINATOR = Pattern.compile("\\n|\\r\\n?");
61  
62      // For now, we always keep both full text and lines array.
63      // In the long run, however, the one passed at initialization might be
64      // enough, while the other could be lazily created when requested.
65      // This would save memory but cost CPU cycles.
66  
67      /**
68       * The name of the file.
69       * {@code null} if no file name is available for whatever reason.
70       */
71      private final File file;
72  
73      /**
74       * The charset used to read the file.
75       * {@code null} if the file was reconstructed from a list of lines.
76       */
77      private final Charset charset;
78  
79      /**
80       * The lines of the file, without terminators.
81       */
82      private final String[] lines;
83  
84      /**
85       * The full text contents of the file.
86       *
87       * @noinspection FieldMayBeFinal
88       * @noinspectionreason FieldMayBeFinal - field is not final to ease reaching full test coverage.
89       */
90      private String fullText;
91  
92      /**
93       * The first position of each line within the full text.
94       */
95      private int[] lineBreaks;
96  
97      /**
98       * Copy constructor.
99       *
100      * @param fileText to make copy of
101      */
102     public FileText(FileText fileText) {
103         file = fileText.file;
104         charset = fileText.charset;
105         fullText = fileText.fullText;
106         lines = fileText.lines.clone();
107         if (fileText.lineBreaks != null) {
108             lineBreaks = fileText.lineBreaks.clone();
109         }
110     }
111 
112     /**
113      * Compatibility constructor.
114      *
115      * <p>This constructor reconstructs the text of the file by joining
116      * lines with linefeed characters. This process does not restore
117      * the original line terminators and should therefore be avoided.
118      *
119      * @param file the name of the file
120      * @param lines the lines of the text, without terminators
121      * @throws NullPointerException if the lines array is null
122      */
123     public FileText(File file, List<String> lines) {
124         final StringBuilder buf = new StringBuilder(1024);
125         for (final String line : lines) {
126             buf.append(line).append('\n');
127         }
128 
129         this.file = file;
130         charset = null;
131         fullText = buf.toString();
132         this.lines = lines.toArray(String[]::new);
133     }
134 
135     /**
136      * Creates a new file text representation.
137      *
138      * <p>The file will be read using the specified encoding, replacing
139      * malformed input and unmappable characters with the default replacement character.
140      *
141      * @param file the name of the file
142      * @param charsetName the encoding to use when reading the file
143      * @throws IOException if the file could not be read
144      * @throws IllegalStateException if the charset is not supported.
145      * @throws NullPointerException if the text is null
146      */
147     public FileText(File file, String charsetName) throws IOException {
148         this.file = file;
149 
150         // We use our own decoder, to be sure we have complete control
151         // about replacements.
152         final CharsetDecoder decoder;
153         try {
154             charset = Charset.forName(charsetName);
155             decoder = charset.newDecoder();
156             decoder.onMalformedInput(CodingErrorAction.REPLACE);
157             decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
158         }
159         catch (final UnsupportedCharsetException exc) {
160             final String message = "Unsupported charset: " + charsetName;
161             throw new IllegalStateException(message, exc);
162         }
163 
164         fullText = readFile(file, decoder);
165 
166         // Use the BufferedReader to break down the lines as this
167         // is about 30% faster than using the
168         // LINE_TERMINATOR.split(fullText, -1) method
169         try (BufferedReader reader = new BufferedReader(new StringReader(fullText))) {
170             final List<String> textLines = new ArrayList<>(10);
171             while (true) {
172                 final String line = reader.readLine();
173                 if (line == null) {
174                     break;
175                 }
176                 textLines.add(line);
177             }
178             lines = textLines.toArray(String[]::new);
179         }
180     }
181 
182     /**
183      * Reads file using specific decoder and returns all its content as a String.
184      *
185      * @param inputFile File to read
186      * @param decoder Charset decoder
187      * @return File's text
188      * @throws FileNotFoundException when inputFile does not exist
189      * @throws IOException Unable to open or read the file
190      */
191     private static String readFile(final File inputFile, final CharsetDecoder decoder)
192             throws IOException {
193         if (!inputFile.exists()) {
194             throw new FileNotFoundException(inputFile.getPath() + " (No such file or directory)");
195         }
196         final StringBuilder buf = new StringBuilder(1024);
197         final InputStream stream = Files.newInputStream(inputFile.toPath());
198         try (Reader reader = new InputStreamReader(stream, decoder)) {
199             final char[] chars = new char[READ_BUFFER_SIZE];
200             while (true) {
201                 final int len = reader.read(chars);
202                 if (len == -1) {
203                     break;
204                 }
205                 buf.append(chars, 0, len);
206             }
207         }
208         return buf.toString();
209     }
210 
211     /**
212      * Retrieves a line of the text by its number.
213      * The returned line will not contain a trailing terminator.
214      *
215      * @param lineNo the number of the line to get, starting at zero
216      * @return the line with the given number
217      */
218     public String get(final int lineNo) {
219         return lines[lineNo];
220     }
221 
222     /**
223      * Get the name of the file.
224      *
225      * @return an object containing the name of the file
226      */
227     public File getFile() {
228         return file;
229     }
230 
231     /**
232      * Get the character set which was used to read the file.
233      * Will be {@code null} for a file reconstructed from its lines.
234      *
235      * @return the charset used when the file was read
236      */
237     public Charset getCharset() {
238         return charset;
239     }
240 
241     /**
242      * Retrieve the full text of the file.
243      *
244      * @return the full text of the file
245      */
246     public CharSequence getFullText() {
247         return fullText;
248     }
249 
250     /**
251      * Returns an array of all lines.
252      * {@code text.toLinesArray()} is equivalent to
253      * {@code text.toArray(new String[text.size()])}.
254      *
255      * @return an array of all lines of the text
256      */
257     public String[] toLinesArray() {
258         return lines.clone();
259     }
260 
261     /**
262      * Determine line and column numbers in full text.
263      *
264      * @param pos the character position in the full text
265      * @return the line and column numbers of this character
266      */
267     public LineColumn lineColumn(int pos) {
268         final int[] lineBreakPositions = findLineBreaks();
269         int lineNo = Arrays.binarySearch(lineBreakPositions, pos);
270         if (lineNo < 0) {
271             // we have: lineNo = -(insertion point) - 1
272             // we want: lineNo =  (insertion point) - 1
273             lineNo = -lineNo - 2;
274         }
275         final int startOfLine = lineBreakPositions[lineNo];
276         final int columnNo = pos - startOfLine;
277         // now we have lineNo and columnNo, both starting at zero.
278         return new LineColumn(lineNo + 1, columnNo);
279     }
280 
281     /**
282      * Find positions of line breaks in the full text.
283      *
284      * @return an array giving the first positions of each line.
285      */
286     private int[] findLineBreaks() {
287         if (lineBreaks == null) {
288             final int[] lineBreakPositions = new int[size() + 1];
289             lineBreakPositions[0] = 0;
290             int lineNo = 1;
291             final Matcher matcher = LINE_TERMINATOR.matcher(fullText);
292             while (matcher.find()) {
293                 lineBreakPositions[lineNo] = matcher.end();
294                 lineNo++;
295             }
296             if (lineNo < lineBreakPositions.length) {
297                 lineBreakPositions[lineNo] = fullText.length();
298             }
299             lineBreaks = lineBreakPositions;
300         }
301         return lineBreaks;
302     }
303 
304     /**
305      * Counts the lines of the text.
306      *
307      * @return the number of lines in the text
308      */
309     public int size() {
310         return lines.length;
311     }
312 
313 }