001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 * <p>
044 * This class will be passed to instances of class FileSetCheck by
045 * Checker.
046 * </p>
047 *
048 */
049public final class FileText {
050
051    /**
052     * The number of characters to read in one go.
053     */
054    private static final int READ_BUFFER_SIZE = 1024;
055
056    /**
057     * Regular expression pattern matching all line terminators.
058     */
059    private static final Pattern LINE_TERMINATOR = Pattern.compile("\\n|\\r\\n?");
060
061    // For now, we always keep both full text and lines array.
062    // In the long run, however, the one passed at initialization might be
063    // enough, while the other could be lazily created when requested.
064    // This would save memory but cost CPU cycles.
065
066    /**
067     * The name of the file.
068     * {@code null} if no file name is available for whatever reason.
069     */
070    private final File file;
071
072    /**
073     * The charset used to read the file.
074     * {@code null} if the file was reconstructed from a list of lines.
075     */
076    private final Charset charset;
077
078    /**
079     * The full text contents of the file.
080     */
081    private final String fullText;
082
083    /**
084     * The lines of the file, without terminators.
085     */
086    private final String[] lines;
087
088    /**
089     * The first position of each line within the full text.
090     */
091    private int[] lineBreaks;
092
093    /**
094     * Creates a new file text representation.
095     *
096     * <p>The file will be read using the specified encoding, replacing
097     * malformed input and unmappable characters with the default
098     * replacement character.
099     *
100     * @param file the name of the file
101     * @param charsetName the encoding to use when reading the file
102     * @throws NullPointerException if the text is null
103     * @throws IOException if the file could not be read
104     */
105    public FileText(File file, String charsetName) throws IOException {
106        this.file = file;
107
108        // We use our own decoder, to be sure we have complete control
109        // about replacements.
110        final CharsetDecoder decoder;
111        try {
112            charset = Charset.forName(charsetName);
113            decoder = charset.newDecoder();
114            decoder.onMalformedInput(CodingErrorAction.REPLACE);
115            decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
116        }
117        catch (final UnsupportedCharsetException ex) {
118            final String message = "Unsupported charset: " + charsetName;
119            throw new IllegalStateException(message, ex);
120        }
121
122        fullText = readFile(file, decoder);
123
124        // Use the BufferedReader to break down the lines as this
125        // is about 30% faster than using the
126        // LINE_TERMINATOR.split(fullText, -1) method
127        try (BufferedReader reader = new BufferedReader(new StringReader(fullText))) {
128            final ArrayList<String> textLines = new ArrayList<>();
129            while (true) {
130                final String line = reader.readLine();
131                if (line == null) {
132                    break;
133                }
134                textLines.add(line);
135            }
136            lines = textLines.toArray(new String[textLines.size()]);
137        }
138    }
139
140    /**
141     * Copy constructor.
142     * @param fileText to make copy of
143     */
144    public FileText(FileText fileText) {
145        file = fileText.file;
146        charset = fileText.charset;
147        fullText = fileText.fullText;
148        lines = fileText.lines.clone();
149        if (fileText.lineBreaks == null) {
150            lineBreaks = null;
151        }
152        else {
153            lineBreaks = fileText.lineBreaks.clone();
154        }
155    }
156
157    /**
158     * Compatibility constructor.
159     *
160     * <p>This constructor reconstructs the text of the file by joining
161     * lines with linefeed characters. This process does not restore
162     * the original line terminators and should therefore be avoided.
163     *
164     * @param file the name of the file
165     * @param lines the lines of the text, without terminators
166     * @throws NullPointerException if the lines array is null
167     */
168    public FileText(File file, List<String> lines) {
169        final StringBuilder buf = new StringBuilder(1024);
170        for (final String line : lines) {
171            buf.append(line).append('\n');
172        }
173
174        this.file = file;
175        charset = null;
176        fullText = buf.toString();
177        this.lines = lines.toArray(new String[lines.size()]);
178    }
179
180    /**
181     * Reads file using specific decoder and returns all its content as a String.
182     * @param inputFile File to read
183     * @param decoder Charset decoder
184     * @return File's text
185     * @throws IOException Unable to open or read the file
186     */
187    private static String readFile(final File inputFile, final CharsetDecoder decoder)
188            throws IOException {
189        if (!inputFile.exists()) {
190            throw new FileNotFoundException(inputFile.getPath() + " (No such file or directory)");
191        }
192        final StringBuilder buf = new StringBuilder(1024);
193        final InputStream stream = Files.newInputStream(inputFile.toPath());
194        try (Reader reader = new InputStreamReader(stream, decoder)) {
195            final char[] chars = new char[READ_BUFFER_SIZE];
196            while (true) {
197                final int len = reader.read(chars);
198                if (len == -1) {
199                    break;
200                }
201                buf.append(chars, 0, len);
202            }
203        }
204        return buf.toString();
205    }
206
207    /**
208     * Get the name of the file.
209     * @return an object containing the name of the file
210     */
211    public File getFile() {
212        return file;
213    }
214
215    /**
216     * Get the character set which was used to read the file.
217     * Will be {@code null} for a file reconstructed from its lines.
218     * @return the charset used when the file was read
219     */
220    public Charset getCharset() {
221        return charset;
222    }
223
224    /**
225     * Retrieve the full text of the file.
226     * @return the full text of the file
227     */
228    public CharSequence getFullText() {
229        return fullText;
230    }
231
232    /**
233     * Returns an array of all lines.
234     * {@code text.toLinesArray()} is equivalent to
235     * {@code text.toArray(new String[text.size()])}.
236     * @return an array of all lines of the text
237     */
238    public String[] toLinesArray() {
239        return lines.clone();
240    }
241
242    /**
243     * Find positions of line breaks in the full text.
244     * @return an array giving the first positions of each line.
245     */
246    private int[] findLineBreaks() {
247        if (lineBreaks == null) {
248            final int[] lineBreakPositions = new int[size() + 1];
249            lineBreakPositions[0] = 0;
250            int lineNo = 1;
251            final Matcher matcher = LINE_TERMINATOR.matcher(fullText);
252            while (matcher.find()) {
253                lineBreakPositions[lineNo] = matcher.end();
254                lineNo++;
255            }
256            if (lineNo < lineBreakPositions.length) {
257                lineBreakPositions[lineNo] = fullText.length();
258            }
259            lineBreaks = lineBreakPositions;
260        }
261        return lineBreaks;
262    }
263
264    /**
265     * Determine line and column numbers in full text.
266     * @param pos the character position in the full text
267     * @return the line and column numbers of this character
268     */
269    public LineColumn lineColumn(int pos) {
270        final int[] lineBreakPositions = findLineBreaks();
271        int lineNo = Arrays.binarySearch(lineBreakPositions, pos);
272        if (lineNo < 0) {
273            // we have: lineNo = -(insertion point) - 1
274            // we want: lineNo =  (insertion point) - 1
275            lineNo = -lineNo - 2;
276        }
277        final int startOfLine = lineBreakPositions[lineNo];
278        final int columnNo = pos - startOfLine;
279        // now we have lineNo and columnNo, both starting at zero.
280        return new LineColumn(lineNo + 1, columnNo);
281    }
282
283    /**
284     * Retrieves a line of the text by its number.
285     * The returned line will not contain a trailing terminator.
286     * @param lineNo the number of the line to get, starting at zero
287     * @return the line with the given number
288     */
289    public String get(final int lineNo) {
290        return lines[lineNo];
291    }
292
293    /**
294     * Counts the lines of the text.
295     * @return the number of lines in the text
296     */
297    public int size() {
298        return lines.length;
299    }
300
301}