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}