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 }