001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.api;
021
022import java.io.BufferedReader;
023import java.io.File;
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.io.Reader;
029import java.io.StringReader;
030import java.nio.charset.Charset;
031import java.nio.charset.CharsetDecoder;
032import java.nio.charset.CodingErrorAction;
033import java.nio.charset.UnsupportedCharsetException;
034import java.nio.file.Files;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.List;
038import java.util.regex.Matcher;
039import java.util.regex.Pattern;
040
041/**
042 * Represents the text contents of a file of arbitrary plain text type.
043 *
044 * <p>
045 * This class will be passed to instances of class FileSetCheck by
046 * Checker.
047 * </p>
048 *
049 */
050public final class FileText {
051
052    /**
053     * The number of characters to read in one go.
054     */
055    private static final int READ_BUFFER_SIZE = 1024;
056
057    /**
058     * Regular expression pattern matching all line terminators.
059     */
060    private static final Pattern LINE_TERMINATOR = Pattern.compile("\\n|\\r\\n?");
061
062    // For now, we always keep both full text and lines array.
063    // In the long run, however, the one passed at initialization might be
064    // enough, while the other could be lazily created when requested.
065    // This would save memory but cost CPU cycles.
066
067    /**
068     * The name of the file.
069     * {@code null} if no file name is available for whatever reason.
070     */
071    private final File file;
072
073    /**
074     * The charset used to read the file.
075     * {@code null} if the file was reconstructed from a list of lines.
076     */
077    private final Charset charset;
078
079    /**
080     * The lines of the file, without terminators.
081     */
082    private final String[] lines;
083
084    /**
085     * The full text contents of the file.
086     *
087     * @noinspection FieldMayBeFinal
088     * @noinspectionreason FieldMayBeFinal - field is not final to ease reaching full test coverage.
089     */
090    private String fullText;
091
092    /**
093     * The first position of each line within the full text.
094     */
095    private int[] lineBreaks;
096
097    /**
098     * Copy constructor.
099     *
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}