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}